Compare commits
559 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 11f0e8c84e | |||
| dd19c866d4 | |||
| 801fed9843 | |||
| 847b5c0917 | |||
| c64056ca7b | |||
| 269c3e29b5 | |||
| 002bbb4a4a | |||
| 14011a5bc2 | |||
| 23f2242c9a | |||
| 4fd92a4002 | |||
| 9c57089b0e | |||
| 3b11cdc72a | |||
| 4edaad2cf4 | |||
| 92fc0621e2 | |||
| e9f4392c48 | |||
| 4df40becaf | |||
| ba11e7c7d7 | |||
| ae671f22c6 | |||
| f22f58212b | |||
| 54ff1c360d | |||
| 772d6c3ef3 | |||
| a94c3b3c26 | |||
| d0ffc4ceb1 | |||
| 597ce487aa | |||
| 87eb8d94fd | |||
| 4944fc516a | |||
| 7537384339 | |||
| 8fd134512f | |||
| f566fc6475 | |||
| 9cbcb48a9b | |||
| 817956cef3 | |||
| 3c1aa7aa42 | |||
| 7598e8f160 | |||
| ba011e7242 | |||
| ecdf20db8f | |||
| 3a7f39321c | |||
| e75d83e7a4 | |||
| 0461c49d23 | |||
| c4f7fa5332 | |||
| 3a9f304735 | |||
| 1d5cb1744f | |||
| b61b0b5451 | |||
| e3a4631e44 | |||
| d7f798e9a0 | |||
| ae2c83e32b | |||
| 059b2ed4f0 | |||
| 338658cd9f | |||
| 2cdb4a6bc2 | |||
| 1a4d5a7954 | |||
| 22ef3e7cd9 | |||
| 5325e707e6 | |||
| 1dc0a6b76a | |||
| 2689d68bab | |||
| f8b8176988 | |||
| 0c5bf2bfe9 | |||
| 218a12962f | |||
| 916d2ec76a | |||
| 1b1ba5deef | |||
| 58b2df4996 | |||
| 8b45517915 | |||
| 3cca07fefd | |||
| 276ae23f13 | |||
| c1b2ffeb78 | |||
| 3b812be84e | |||
| 262ee34201 | |||
| 5d86bd5d02 | |||
| 3b6271c4f4 | |||
| 1671a3d50f | |||
| 0ab318e804 | |||
| 53719bd94a | |||
| 935b7d9e10 | |||
| e3bf19b972 | |||
| c36f09d3e9 | |||
| 9936a89118 | |||
| 562424cd8f | |||
| 0c3fb83d0a | |||
| 7df352a44b | |||
| 1443d377ad | |||
| e0bca25411 | |||
| e94d47b1a5 | |||
| 9d3a1674c4 | |||
| 24d81a4153 | |||
| 5723a8017b | |||
| 72055cd998 | |||
| 9cd8b4d960 | |||
| f39b368c1e | |||
| a4543351fe | |||
| 88ac013166 | |||
| 5d336b9090 | |||
| 45b3209b9b | |||
| da16cf6ada | |||
| 774a841fb5 | |||
| 020ffdaa84 | |||
| 2ef096b3a0 | |||
| 69bde3e80d | |||
| e6aa7db35a | |||
| 0124d98111 | |||
| c00a3836a5 | |||
| 66f9118e78 | |||
| f261b90a73 | |||
| a1602e0221 | |||
| b4e9ce8478 | |||
| 35d7cbb1d3 | |||
| 6ab678bb18 | |||
| 14ec9cf0a1 | |||
| a225bf2cf1 | |||
| 9b904cd895 | |||
| 11a16092a7 | |||
| b9b7f940b9 | |||
| e542728ff6 | |||
| 1eeb6e2d73 | |||
| cf9d53aca2 | |||
| 83463c792c | |||
| d5c7571a97 | |||
| 75c27bc232 | |||
| cbcc35268d | |||
| 11ba247ac4 | |||
| f411e81949 | |||
| 45d7cb7218 | |||
| 81f42d3951 | |||
| 2f7aed3e34 | |||
| 01f8261f0b | |||
| 489d7696cc | |||
| 0661f08a07 | |||
| 206475e6ab | |||
| a4a68e5b39 | |||
| de7ab0e0cb | |||
| 5f7b5ce81c | |||
| 36d7ea5100 | |||
| 33f8cf3802 | |||
| f6da876fdf | |||
| 96c5dc7b74 | |||
| a37931b62d | |||
| 5c848d9bf5 | |||
| 265b805fa9 | |||
| c4dfe33a5e | |||
| 999eeb47ba | |||
| a5bf02cfec | |||
| c10186e50a | |||
| 7a1cbf99d4 | |||
| c9c4000536 | |||
| 656bbc1b1c | |||
| 3815a08af5 | |||
| 404fdb6496 | |||
| 573f355c15 | |||
| bd0c891795 | |||
| f4857a3da9 | |||
| e074c3caf1 | |||
| 0f4c7503b1 | |||
| 2cb142a31f | |||
| 2b05ea8d92 | |||
| d255e1ea4a | |||
| 23846dbf23 | |||
| 6fcb817fb8 | |||
| e2ebfd01df | |||
| cf70929204 | |||
| 624e3f7553 | |||
| 3cc8538db3 | |||
| a9a65436bb | |||
| 353dabfc47 | |||
| 1bdf0f5b78 | |||
| 6696d7f71d | |||
| 59a6c7e576 | |||
| 0b0148635e | |||
| c354d8ea92 | |||
| 9562cdaccb | |||
| bcfff8a33a | |||
| f5c409ba90 | |||
| 2f23520330 | |||
| a95694cbfc | |||
| 6cfba6495f | |||
| b9f3de1c64 | |||
| c67b56e7af | |||
| bf39f07691 | |||
| a9e46f0de6 | |||
| 05478fa8fa | |||
| d123afc3f7 | |||
| b1b3c15300 | |||
| 49bae50739 | |||
| 9b5459af8e | |||
| 96d2ea57f5 | |||
| b78c5f4ece | |||
| 341522ccaa | |||
| 833628e42f | |||
| 50c1c6fdc9 | |||
| 213386e822 | |||
| 17317ed274 | |||
| 652ae8c967 | |||
| ec0e8cbdaf | |||
| c15d4c9035 | |||
| 39dfdd0ef0 | |||
| d25bc58249 | |||
| 194a197e83 | |||
| bd02f34bcf | |||
| 7d5f03bb6e | |||
| 60f41e168f | |||
| 680b16aa25 | |||
| 2394c03dca | |||
| c8f211f2bf | |||
| 9076ba8933 | |||
| 08e79bfc5c | |||
| e4808516aa | |||
| 5888104e98 | |||
| 2b8d29759a | |||
| 8e4b56ff17 | |||
| 23c5761c14 | |||
| 835ec0c3a0 | |||
| d2dce0b6f8 | |||
| 3f8628af01 | |||
| 40c6fae07c | |||
| e1185fda93 | |||
| 458ed903c5 | |||
| 4b640f4f01 | |||
| 8743c5efd0 | |||
| 9bd9d31d40 | |||
| 7e3d720720 | |||
| 7a512191ab | |||
| af767863bb | |||
| 1b233f4f87 | |||
| 3a840df79e | |||
| 6851e3b072 | |||
| 5a2b079872 | |||
| f683f987d8 | |||
| 3626621e87 | |||
| cc2d1eb002 | |||
| 5ba19bcf8b | |||
| 8391686e28 | |||
| 38327c917f | |||
| fca0f399b2 | |||
| e3b7ac0e99 | |||
| 04cd357f68 | |||
| 68f48f714d | |||
| fa586c494f | |||
| b886a0da64 | |||
| 0616aec3d2 | |||
| 5df36a6601 | |||
| 7d6f295e72 | |||
| 1c952b6dcb | |||
| 7e7c2a0fd7 | |||
| a15444d237 | |||
| 5b100f3b22 | |||
| 9fa8a48c56 | |||
| cb54a2a7e1 | |||
| ed02713ccc | |||
| 68e3fd2093 | |||
| a43f73d7b1 | |||
| 61e0c4ed0a | |||
| 53e0629b1d | |||
| 2b1d6a3d8a | |||
| 5ba1fb3d95 | |||
| 1b7c15cdb5 | |||
| 81fd85e993 | |||
| 8f4c08d5b3 | |||
| 61b6429851 | |||
| bbc6b39c08 | |||
| 3f812f4d6d | |||
| c9b15e4239 | |||
| 5fbdb3d481 | |||
| c59e1cd7fc | |||
| ce891e47da | |||
| 7160e4a42e | |||
| eba857a285 | |||
| a1dd02c780 | |||
| 87ff5d629b | |||
| 6b75523428 | |||
| 773434d4f6 | |||
| ad6dcd0314 | |||
| b9e29ad87d | |||
| 32b965ba87 | |||
| f1b315c9ea | |||
| 459fc75af3 | |||
| 9b0cd3511f | |||
| af9b988507 | |||
| 36f297c35b | |||
| ff959f71a7 | |||
| 0a4312ada6 | |||
| 5411cdc07a | |||
| a8c6fba3c1 | |||
| 11b115b47b | |||
| 22edf5ce46 | |||
| f43f7df7f3 | |||
| 3a2633d818 | |||
| 04a62bcf74 | |||
| 6c1320168c | |||
| 8657c91002 | |||
| bafe492009 | |||
| c6197ef6a3 | |||
| 1b3f16bcd5 | |||
| 28712fdeca | |||
| 0c30b68a9e | |||
| 30c4bee432 | |||
| ece9ced085 | |||
| f231105752 | |||
| 91dfc1e086 | |||
| b2c59c17aa | |||
| 10d1a920f0 | |||
| 4cb79a14fc | |||
| 402b9bd8dc | |||
| c39cc9d93b | |||
| aad56ab0a7 | |||
| fe18e493a9 | |||
| 5d14166508 | |||
| e1a745e3b5 | |||
| a0cac28ed0 | |||
| c205dc8672 | |||
| 5c0ed4cf7d | |||
| 780472a17f | |||
| 0264db3d54 | |||
| c117594669 | |||
| 5214bd8936 | |||
| a1195be08e | |||
| b69d366538 | |||
| c9de6f0dc3 | |||
| 21be693a9a | |||
| 129362fcd0 | |||
| 75e2fcc3ce | |||
| 4f56b57b0e | |||
| f9bbdf3427 | |||
| fcf200e169 | |||
| 7d668c8525 | |||
| ebd4a32bfc | |||
| 6aa739231d | |||
| 8877d32ced | |||
| 7f025ae845 | |||
| 1b2dae2135 | |||
| 6e4e9df616 | |||
| 49e868a505 | |||
| f1b70e0367 | |||
| 1d0e747578 | |||
| 2c72d07cab | |||
| 31c057f9f8 | |||
| d3033df9da | |||
| 459d82b1c6 | |||
| 85d7ca640e | |||
| c1b7f2f092 | |||
| b7a7e0d4ad | |||
| dc7f6d58f9 | |||
| a2a10bd0d3 | |||
| b54c8ee6ee | |||
| 08d275690a | |||
| 1c307f751e | |||
| 6f06a0f7fc | |||
| a095ace30e | |||
| a486f61f5f | |||
| 14e0abc240 | |||
| 32203c48bd | |||
| 9d6024f603 | |||
| a4dd4e48e7 | |||
| e6f7456a0f | |||
| fca79c9b0c | |||
| 4ad7f11e93 | |||
| 0412bdc996 | |||
| 31faeaada3 | |||
| 72539ca973 | |||
| ef94630aa1 | |||
| cb696f9992 | |||
| aa23e404e1 | |||
| 4c0749640f | |||
| ee5661f304 | |||
| ddefdc4f34 | |||
| f2a0af1646 | |||
| 7ded61c2bc | |||
| 08aabcf6dd | |||
| e19af7e67d | |||
| 62aa07e28e | |||
| 03b0bf747e | |||
| 8dc75aed55 | |||
| d5f5e99010 | |||
| 18c46d191e | |||
| e9f92430b2 | |||
| 3106865449 | |||
| 375e7a59e2 | |||
| bc4a2def42 | |||
| 8204a6cf27 | |||
| 797292dbe5 | |||
| 6e7a33b3a1 | |||
| 2bfea00c72 | |||
| 81441a724b | |||
| 82c8d8dd9a | |||
| 8751a9fe53 | |||
| 9cf561fcf1 | |||
| 332559c67d | |||
| 56557f0092 | |||
| 1e6cb7b1ad | |||
| 7f419b7e78 | |||
| 973a90e071 | |||
| 66a8ca36e4 | |||
| 3715007156 | |||
| 5158685c02 | |||
| 61d3371ea5 | |||
| 0491507e67 | |||
| fcd4ad874a | |||
| e2668fcdf2 | |||
| f3ac2b2cec | |||
| 8b84391e36 | |||
| 1b3ca347f5 | |||
| 2dced7bfbf | |||
| 8ba4ce36a1 | |||
| cf60b09225 | |||
| 427ec45d42 | |||
| ac9f8fe89c | |||
| 6817990555 | |||
| d395cde316 | |||
| 20272eccb8 | |||
| 091ae8abff | |||
| 6e87690649 | |||
| d5a1bd3859 | |||
| a1e4643a25 | |||
| 71c0450614 | |||
| d469caad69 | |||
| 5cc3d4fbfb | |||
| a8691ee3a5 | |||
| 91d7941921 | |||
| 0bc7a0953e | |||
| c60bea5952 | |||
| 4db648ad25 | |||
| 0bb8342873 | |||
| d4f2a88fdf | |||
| af45d39841 | |||
| 66f5b0b210 | |||
| 4a3b79f1b4 | |||
| 12a100def8 | |||
| 47971f607a | |||
| 03a4d342a3 | |||
| 4f5abfefec | |||
| e1ee3c06e8 | |||
| 17ba704472 | |||
| e5391fa1f4 | |||
| c0647017b5 | |||
| 3686bb4b44 | |||
| 76c8ca4b20 | |||
| c53e64027b | |||
| c15d236320 | |||
| 281504c9c6 | |||
| eaf0ebe62b | |||
| a4a08662be | |||
| e8a0ffeca5 | |||
| 1399cc6fbd | |||
| 8b44ad4b08 | |||
| e4a1a6e293 | |||
| ba00786b91 | |||
| 5e7529d1e6 | |||
| e75108113a | |||
| 6d51b0d420 | |||
| c3b199755e | |||
| d57d8e9da5 | |||
| d24e1c5355 | |||
| 95c94560be | |||
| 7b4ed52eb1 | |||
| cab8c15474 | |||
| 72f5d59a75 | |||
| 273adc8d1b | |||
| 5f0f28cb0e | |||
| e1c9fe120b | |||
| 630580beb6 | |||
| 68a2c43580 | |||
| 817fce6d10 | |||
| 6e85afaee6 | |||
| 98c5096f67 | |||
| 57c7ced59d | |||
| 73e6d38344 | |||
| bd128bf8b0 | |||
| 16e8808ce5 | |||
| f4088fcb6b | |||
| 63b8aa24e8 | |||
| 5744491606 | |||
| f5ecbef724 | |||
| f176a2c70e | |||
| 51c124d3e4 | |||
| 5f6c97336e | |||
| 894eb77d5d | |||
| d1b5a1f517 | |||
| 300d5f8d91 | |||
| bbc885f783 | |||
| 6badeeebe5 | |||
| e282806422 | |||
| 629807584b | |||
| 922c0e53d2 | |||
| ec9fcd473a | |||
| de9f415ded | |||
| fd5ca2c2fc | |||
| c0b9ddc4a3 | |||
| 43f33083f1 | |||
| 2b483e6adb | |||
| 0cf0f42ca4 | |||
| c9ccea3f84 | |||
| c2dee28132 | |||
| 4fd4709182 | |||
| 00ccc0eb6a | |||
| ed91f51482 | |||
| c2cea95aa5 | |||
| 274027cb64 | |||
| b4a26344d9 | |||
| 580c708788 | |||
| f4d6380094 | |||
| e44dc06a61 | |||
| b6184f5b41 | |||
| e6fc2f397e | |||
| 60d0b62675 | |||
| 7a8eb1833f | |||
| 6367b76b9d | |||
| 977b685071 | |||
| b97d418158 | |||
| 0e4cb372d5 | |||
| 2ec7576ae9 | |||
| c4bf4c3067 | |||
| ea9bbcad27 | |||
| 71be1f2ed5 | |||
| 349bb91c6c | |||
| 52da673358 | |||
| ce0b9d1413 | |||
| 8ef332f3e5 | |||
| a4002f83c1 | |||
| 64d756d8a9 | |||
| 187fe47268 | |||
| 060f3a0b1b | |||
| 4dcc5bc564 | |||
| 52efac6643 | |||
| 97c91fb7aa | |||
| efcc598550 | |||
| fd5fc1f485 | |||
| f713d4057f | |||
| 4ebbea8e86 | |||
| 5515e6f788 | |||
| 65a6315f1b | |||
| aafe32bb3d | |||
| 1c6c783dbe | |||
| 1e322f47d4 | |||
| 37196abe77 | |||
| e476cf5ce4 | |||
| dc4b1e7a90 | |||
| 95d188d5f1 | |||
| 4dd60ca855 | |||
| 5067917295 | |||
| e620ef27ee | |||
| 868dc17425 | |||
| 03966f356f | |||
| c28ab32874 | |||
| 32b7ab64d5 | |||
| 79d8e1851a | |||
| e3c1743b57 | |||
| ed257bf5b7 | |||
| f918b8709e | |||
| ad46f5bd55 | |||
| 4e2f9bc349 | |||
| a3e8d1587b | |||
| ce556e213c | |||
| 7c3581d8be | |||
| 8c53fd4869 | |||
| 540862e95a | |||
| e08ce7fe18 | |||
| be2a455088 | |||
| 6333dfacb1 | |||
| 56aa1c405c | |||
| 55b76a5fca | |||
| c9453655d5 | |||
| f241227e7a | |||
| f304bf0362 | |||
| 7d1c12d3a6 |
@@ -0,0 +1,27 @@
|
||||
> Please fill out this template appropriately when filing a bug report.
|
||||
>
|
||||
> Please remove this line and everything above it before submitting.
|
||||
|
||||
### Short description
|
||||
|
||||
### Expected behavior
|
||||
|
||||
### Actual behavior
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
**Code example that reproduces the issue**
|
||||
|
||||
### Environment
|
||||
|
||||
**Library version**
|
||||
|
||||
**Installation method**
|
||||
|
||||
- [ ] CocoaPods
|
||||
- [ ] Carthage
|
||||
- [ ] Git submodules
|
||||
|
||||
**iOS version(s)**
|
||||
|
||||
**Xcode version**
|
||||
@@ -22,6 +22,7 @@ xcuserdata/
|
||||
*.moved-aside
|
||||
*.xccheckout
|
||||
*.xcscmblueprint
|
||||
*.xcsettings
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
language: objective-c
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
cache:
|
||||
directories:
|
||||
- /usr/local/Homebrew
|
||||
- $HOME/Library/Caches/Homebrew
|
||||
before_cache:
|
||||
- brew cleanup
|
||||
env:
|
||||
global:
|
||||
- LANG=en_US.UTF-8
|
||||
- LC_ALL=en_US.UTF-8
|
||||
jobs:
|
||||
include:
|
||||
- stage: "Builds"
|
||||
osx_image: xcode9.4
|
||||
script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=4.1 clean build
|
||||
name: "Swift 4.1"
|
||||
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=4.2 clean build
|
||||
osx_image: xcode10
|
||||
name: "Swift 4.2"
|
||||
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.0 clean build
|
||||
osx_image: xcode10.3
|
||||
name: "Swift 5.0"
|
||||
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.1 SUPPORTS_MACCATALYST=NO clean build
|
||||
# SUPPORTS_MACCATALYST=NO because Xcode 11 runs on macOS 10.14 in Travis CI
|
||||
osx_image: xcode11
|
||||
name: "Swift 5.1"
|
||||
|
||||
- stage: "Tests"
|
||||
osx_image: xcode10.3
|
||||
script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=10.3.1,name=iPhone SE'
|
||||
name: "iPhone SE (iOS 10.3)"
|
||||
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=11.4,name=iPhone 7'
|
||||
osx_image: xcode10.3
|
||||
name: "iPhone 7 (iOS 11.4)"
|
||||
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=12.2,name=iPhone X'
|
||||
osx_image: xcode10.3
|
||||
name: "iPhone X (iOS 12.2)"
|
||||
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=13.0,name=iPhone 11'
|
||||
osx_image: xcode11
|
||||
name: "iPhone X (iOS 13.0)"
|
||||
|
||||
- stage: Build examples
|
||||
osx_image: xcode11
|
||||
script: xcodebuild -scheme Maps -sdk iphonesimulator clean build
|
||||
name: "Maps"
|
||||
- script: xcodebuild -scheme Stocks -sdk iphonesimulator clean build
|
||||
osx_image: xcode11
|
||||
name: "Stocks"
|
||||
- script: xcodebuild -scheme Samples -sdk iphonesimulator clean build
|
||||
osx_image: xcode11
|
||||
name: "Samples"
|
||||
|
||||
- stage: Carthage
|
||||
osx_image: xcode11
|
||||
before_install:
|
||||
- brew update
|
||||
- brew outdated carthage || brew upgrade carthage
|
||||
script:
|
||||
- carthage build --no-skip-current
|
||||
|
||||
- stage: CocoaPods
|
||||
osx_image: xcode11
|
||||
before_install:
|
||||
- gem install cocoapods
|
||||
script:
|
||||
- pod spec lint --allow-warnings
|
||||
- pod lib lint --allow-warnings
|
||||
@@ -7,13 +7,14 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
543844BD23D2BE2000D5EDE4 /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 543844BC23D2BE2000D5EDE4 /* MapKit.framework */; };
|
||||
549D23D2233C77D5008EF4D7 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23D1233C77D5008EF4D7 /* FloatingPanel.framework */; };
|
||||
549D23D3233C77D5008EF4D7 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23D1233C77D5008EF4D7 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
54B5112A216C3D840033A6F3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B51129216C3D840033A6F3 /* AppDelegate.swift */; };
|
||||
54B5112C216C3D840033A6F3 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B5112B216C3D840033A6F3 /* ViewController.swift */; };
|
||||
54B5112F216C3D840033A6F3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54B5112D216C3D840033A6F3 /* Main.storyboard */; };
|
||||
54B51131216C3D860033A6F3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 54B51130216C3D860033A6F3 /* Assets.xcassets */; };
|
||||
54B51134216C3D860033A6F3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54B51132216C3D860033A6F3 /* LaunchScreen.storyboard */; };
|
||||
54B5113F216C407F0033A6F3 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54B5113E216C407F0033A6F3 /* FloatingPanel.framework */; };
|
||||
54B51140216C407F0033A6F3 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 54B5113E216C407F0033A6F3 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
@@ -23,7 +24,7 @@
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
54B51140216C407F0033A6F3 /* FloatingPanel.framework in Embed Frameworks */,
|
||||
549D23D3233C77D5008EF4D7 /* FloatingPanel.framework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -31,6 +32,8 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
543844BC23D2BE2000D5EDE4 /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = System/Library/Frameworks/MapKit.framework; sourceTree = SDKROOT; };
|
||||
549D23D1233C77D5008EF4D7 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
54B51126216C3D840033A6F3 /* Maps.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Maps.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
54B51129216C3D840033A6F3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
54B5112B216C3D840033A6F3 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
@@ -38,7 +41,6 @@
|
||||
54B51130216C3D860033A6F3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
54B51133216C3D860033A6F3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
54B51135216C3D860033A6F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
54B5113E216C407F0033A6F3 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -46,19 +48,29 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
54B5113F216C407F0033A6F3 /* FloatingPanel.framework in Frameworks */,
|
||||
543844BD23D2BE2000D5EDE4 /* MapKit.framework in Frameworks */,
|
||||
549D23D2233C77D5008EF4D7 /* FloatingPanel.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
543844BB23D2BE1F00D5EDE4 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
543844BC23D2BE2000D5EDE4 /* MapKit.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54B5111D216C3D840033A6F3 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54B5113E216C407F0033A6F3 /* FloatingPanel.framework */,
|
||||
549D23D1233C77D5008EF4D7 /* FloatingPanel.framework */,
|
||||
54B51128216C3D840033A6F3 /* Maps */,
|
||||
54B51127216C3D840033A6F3 /* Products */,
|
||||
543844BB23D2BE1F00D5EDE4 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -303,7 +315,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = J3D7L9FHSS;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = Maps/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -312,7 +324,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.Maps;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -322,7 +334,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = J3D7L9FHSS;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = Maps/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -331,7 +343,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.Maps;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Maps
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/10/09.
|
||||
// Copyright © 2018 scenee. All rights reserved.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@@ -60,9 +60,9 @@
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Ye3-uU-bq3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="900"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="ED1-gT-FBj">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="900"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<searchBar contentMode="redraw" searchBarStyle="minimal" translatesAutoresizingMaskIntoConstraints="NO" id="Zcj-SE-gb8">
|
||||
@@ -70,7 +70,7 @@
|
||||
<textInputTraits key="textInputTraits"/>
|
||||
</searchBar>
|
||||
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="D7r-re-InH">
|
||||
<rect key="frame" x="0.0" y="66" width="375" height="712"/>
|
||||
<rect key="frame" x="0.0" y="66" width="375" height="748"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<view key="tableHeaderView" contentMode="scaleToFill" id="u28-LY-hIh" customClass="SearchHeaderView" customModule="Maps" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="116"/>
|
||||
@@ -227,7 +227,7 @@
|
||||
<constraint firstAttribute="trailing" secondItem="D7r-re-InH" secondAttribute="trailing" id="BES-GA-Btp"/>
|
||||
<constraint firstItem="D7r-re-InH" firstAttribute="leading" secondItem="ED1-gT-FBj" secondAttribute="leading" id="UTe-YL-17h"/>
|
||||
<constraint firstItem="Zcj-SE-gb8" firstAttribute="top" secondItem="ED1-gT-FBj" secondAttribute="top" constant="6" id="apU-Pd-PEO"/>
|
||||
<constraint firstAttribute="bottom" secondItem="D7r-re-InH" secondAttribute="bottom" id="vfS-Lx-TXz"/>
|
||||
<constraint firstAttribute="bottom" secondItem="D7r-re-InH" secondAttribute="bottom" constant="86" id="vfS-Lx-TXz"/>
|
||||
<constraint firstItem="D7r-re-InH" firstAttribute="top" secondItem="Zcj-SE-gb8" secondAttribute="bottom" constant="4" id="vro-cd-B9c"/>
|
||||
<constraint firstItem="Zcj-SE-gb8" firstAttribute="leading" secondItem="ED1-gT-FBj" secondAttribute="leading" id="wMb-L2-Z0W"/>
|
||||
</constraints>
|
||||
@@ -238,7 +238,7 @@
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Ye3-uU-bq3" firstAttribute="leading" secondItem="G74-X7-Za8" secondAttribute="leading" id="Kr2-sU-ZWZ"/>
|
||||
<constraint firstItem="Ye3-uU-bq3" firstAttribute="bottom" secondItem="G74-X7-Za8" secondAttribute="bottom" id="aWM-s3-3o4"/>
|
||||
<constraint firstItem="Ye3-uU-bq3" firstAttribute="bottom" secondItem="Ncl-E9-yRn" secondAttribute="bottom" constant="88" id="aWM-s3-3o4"/>
|
||||
<constraint firstItem="Ye3-uU-bq3" firstAttribute="trailing" secondItem="G74-X7-Za8" secondAttribute="trailing" id="fEL-8y-Acc"/>
|
||||
<constraint firstItem="Ye3-uU-bq3" firstAttribute="top" secondItem="Ncl-E9-yRn" secondAttribute="top" id="w77-ba-FrJ"/>
|
||||
</constraints>
|
||||
@@ -247,6 +247,7 @@
|
||||
<connections>
|
||||
<outlet property="searchBar" destination="Zcj-SE-gb8" id="BH7-Gy-RG5"/>
|
||||
<outlet property="tableView" destination="D7r-re-InH" id="nRN-fY-b8j"/>
|
||||
<outlet property="visualEffectView" destination="Ye3-uU-bq3" id="rS6-Mq-OKs"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="EqR-Hp-zhc" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright © 2018 scenee. All rights reserved.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
@@ -21,13 +21,17 @@ class ViewController: UIViewController, MKMapViewDelegate, UISearchBarDelegate,
|
||||
|
||||
// Initialize FloatingPanelController and add the view
|
||||
fpc.surfaceView.backgroundColor = .clear
|
||||
fpc.surfaceView.cornerRadius = 9.0
|
||||
if #available(iOS 11, *) {
|
||||
fpc.surfaceView.cornerRadius = 9.0
|
||||
} else {
|
||||
fpc.surfaceView.cornerRadius = 0.0
|
||||
}
|
||||
fpc.surfaceView.shadowHidden = false
|
||||
|
||||
searchVC = storyboard?.instantiateViewController(withIdentifier: "SearchPanel") as? SearchPanelViewController
|
||||
|
||||
// Add a content view controller
|
||||
fpc.show(searchVC, sender: self)
|
||||
// Set a content view controller
|
||||
fpc.set(contentViewController: searchVC)
|
||||
fpc.track(scrollView: searchVC.tableView)
|
||||
|
||||
setupMapView()
|
||||
@@ -36,7 +40,7 @@ class ViewController: UIViewController, MKMapViewDelegate, UISearchBarDelegate,
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
// Add FloatingPanel to a view with animation.
|
||||
fpc.add(toParent: self, animated: true)
|
||||
fpc.addPanel(toParent: self, animated: true)
|
||||
|
||||
// Must be here
|
||||
searchVC.searchBar.delegate = self
|
||||
@@ -84,15 +88,16 @@ class ViewController: UIViewController, MKMapViewDelegate, UISearchBarDelegate,
|
||||
// MARK: FloatingPanelControllerDelegate
|
||||
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
|
||||
switch traitCollection.verticalSizeClass {
|
||||
switch newCollection.verticalSizeClass {
|
||||
case .compact:
|
||||
fpc.surfaceView.borderWidth = 1.0 / traitCollection.displayScale
|
||||
fpc.surfaceView.borderColor = UIColor.black.withAlphaComponent(0.2)
|
||||
return SearchPanelLandscapeLayout()
|
||||
default:
|
||||
fpc.surfaceView.borderWidth = 0.0
|
||||
fpc.surfaceView.borderColor = nil
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func floatingPanelDidMove(_ vc: FloatingPanelController) {
|
||||
@@ -102,6 +107,7 @@ class ViewController: UIViewController, MKMapViewDelegate, UISearchBarDelegate,
|
||||
let progress = max(0.0, min((tipY - y) / 44.0, 1.0))
|
||||
self.searchVC.tableView.alpha = progress
|
||||
}
|
||||
debugPrint("NearbyPosition : ",vc.nearbyPosition)
|
||||
}
|
||||
|
||||
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) {
|
||||
@@ -133,20 +139,46 @@ class SearchPanelViewController: UIViewController, UITableViewDataSource, UITabl
|
||||
|
||||
@IBOutlet weak var tableView: UITableView!
|
||||
@IBOutlet weak var searchBar: UISearchBar!
|
||||
@IBOutlet weak var visualEffectView: UIVisualEffectView!
|
||||
|
||||
// For iOS 10 only
|
||||
private lazy var shadowLayer: CAShapeLayer = CAShapeLayer()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
searchBar.placeholder = "Search for a place or address"
|
||||
let textField = searchBar.value(forKey: "_searchField") as! UITextField
|
||||
textField.font = UIFont(name: textField.font!.fontName, size: 15.0)
|
||||
searchBar.setSearchText(fontSize: 15.0)
|
||||
|
||||
hideHeader()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
if #available(iOS 11, *) {
|
||||
} else {
|
||||
// Exmaple: Add rounding corners on iOS 10
|
||||
visualEffectView.layer.cornerRadius = 9.0
|
||||
visualEffectView.clipsToBounds = true
|
||||
|
||||
// Exmaple: Add shadow manually on iOS 10
|
||||
view.layer.insertSublayer(shadowLayer, at: 0)
|
||||
let rect = visualEffectView.frame
|
||||
let path = UIBezierPath(roundedRect: rect,
|
||||
byRoundingCorners: [.topLeft, .topRight],
|
||||
cornerRadii: CGSize(width: 9.0, height: 9.0))
|
||||
shadowLayer.frame = visualEffectView.frame
|
||||
shadowLayer.shadowPath = path.cgPath
|
||||
shadowLayer.shadowColor = UIColor.black.cgColor
|
||||
shadowLayer.shadowOffset = CGSize(width: 0.0, height: 1.0)
|
||||
shadowLayer.shadowOpacity = 0.2
|
||||
shadowLayer.shadowRadius = 3.0
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return 2
|
||||
return 100
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
@@ -157,12 +189,10 @@ class SearchPanelViewController: UIViewController, UITableViewDataSource, UITabl
|
||||
cell.iconImageView.image = UIImage(named: "mark")
|
||||
cell.titleLabel.text = "Marked Location"
|
||||
cell.subTitleLabel.text = "Golden Gate Bridge, San Francisco"
|
||||
case 1:
|
||||
default:
|
||||
cell.iconImageView.image = UIImage(named: "like")
|
||||
cell.titleLabel.text = "Favorites"
|
||||
cell.subTitleLabel.text = "0 Places"
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return cell
|
||||
@@ -193,6 +223,42 @@ class SearchPanelViewController: UIViewController, UITableViewDataSource, UITabl
|
||||
}
|
||||
}
|
||||
|
||||
public class SearchPanelLandscapeLayout: FloatingPanelLayout {
|
||||
public var initialPosition: FloatingPanelPosition {
|
||||
return .tip
|
||||
}
|
||||
|
||||
public var supportedPositions: Set<FloatingPanelPosition> {
|
||||
return [.full, .tip]
|
||||
}
|
||||
|
||||
public func insetFor(position: FloatingPanelPosition) -> CGFloat? {
|
||||
switch position {
|
||||
case .full: return 16.0
|
||||
case .tip: return 69.0
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
|
||||
if #available(iOS 11.0, *) {
|
||||
return [
|
||||
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0),
|
||||
surfaceView.widthAnchor.constraint(equalToConstant: 291),
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8.0),
|
||||
surfaceView.widthAnchor.constraint(equalToConstant: 291),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
public func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
|
||||
return 0.0
|
||||
}
|
||||
}
|
||||
|
||||
class SearchCell: UITableViewCell {
|
||||
@IBOutlet weak var iconImageView: UIImageView!
|
||||
@IBOutlet weak var titleLabel: UILabel!
|
||||
@@ -205,3 +271,15 @@ class SearchHeaderView: UIView {
|
||||
self.clipsToBounds = true
|
||||
}
|
||||
}
|
||||
|
||||
extension UISearchBar {
|
||||
func setSearchText(fontSize: CGFloat) {
|
||||
#if swift(>=5.1) // Xcode 11 or later
|
||||
let font = searchTextField.font
|
||||
searchTextField.font = font?.withSize(fontSize)
|
||||
#else
|
||||
let textField = value(forKey: "_searchField") as! UITextField
|
||||
textField.font = textField.font?.withSize(fontSize)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,644 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 50;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
545DB9EE21511E6300CA77B8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9ED21511E6300CA77B8 /* AppDelegate.swift */; };
|
||||
545DB9F021511E6300CA77B8 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9EF21511E6300CA77B8 /* ViewController.swift */; };
|
||||
545DB9F321511E6300CA77B8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 545DB9F121511E6300CA77B8 /* Main.storyboard */; };
|
||||
545DB9F521511E6400CA77B8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 545DB9F421511E6400CA77B8 /* Assets.xcassets */; };
|
||||
545DB9F821511E6400CA77B8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 545DB9F621511E6400CA77B8 /* LaunchScreen.storyboard */; };
|
||||
545DBA0321511E6400CA77B8 /* SampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA0221511E6400CA77B8 /* SampleTests.swift */; };
|
||||
545DBA0E21511E6400CA77B8 /* SampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA0D21511E6400CA77B8 /* SampleUITests.swift */; };
|
||||
549D23CB233C7779008EF4D7 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CA233C7779008EF4D7 /* FloatingPanel.framework */; };
|
||||
549D23CC233C7779008EF4D7 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CA233C7779008EF4D7 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
54B51116216AFE5F0033A6F3 /* UIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B51115216AFE5F0033A6F3 /* UIExtensions.swift */; };
|
||||
54CDC5D8215BBE23007D205C /* UIComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D7215BBE23007D205C /* UIComponents.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
545DB9FF21511E6400CA77B8 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 545DB9E221511E6300CA77B8 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 545DB9E921511E6300CA77B8;
|
||||
remoteInfo = FloatingModalSample;
|
||||
};
|
||||
545DBA0A21511E6400CA77B8 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 545DB9E221511E6300CA77B8 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 545DB9E921511E6300CA77B8;
|
||||
remoteInfo = FloatingModalSample;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
549D23CD233C7779008EF4D7 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
549D23CC233C7779008EF4D7 /* FloatingPanel.framework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
545DB9EA21511E6300CA77B8 /* Samples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Samples.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
545DB9ED21511E6300CA77B8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
545DB9EF21511E6300CA77B8 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
545DB9F221511E6300CA77B8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
545DB9F421511E6400CA77B8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
545DB9F721511E6400CA77B8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
545DB9F921511E6400CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
545DB9FE21511E6400CA77B8 /* SamplesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SamplesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
545DBA0221511E6400CA77B8 /* SampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleTests.swift; sourceTree = "<group>"; };
|
||||
545DBA0421511E6400CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
545DBA0921511E6400CA77B8 /* SamplesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SamplesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
545DBA0D21511E6400CA77B8 /* SampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleUITests.swift; sourceTree = "<group>"; };
|
||||
545DBA0F21511E6400CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
549D23CA233C7779008EF4D7 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
54B51115216AFE5F0033A6F3 /* UIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIExtensions.swift; sourceTree = "<group>"; };
|
||||
54CDC5D7215BBE23007D205C /* UIComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIComponents.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
545DB9E721511E6300CA77B8 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
549D23CB233C7779008EF4D7 /* FloatingPanel.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
545DB9FB21511E6400CA77B8 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
545DBA0621511E6400CA77B8 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
545DB9E121511E6300CA77B8 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
549D23CA233C7779008EF4D7 /* FloatingPanel.framework */,
|
||||
545DB9EC21511E6300CA77B8 /* Sources */,
|
||||
545DBA0121511E6400CA77B8 /* Tests */,
|
||||
545DBA0C21511E6400CA77B8 /* UITests */,
|
||||
545DB9EB21511E6300CA77B8 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
545DB9EB21511E6300CA77B8 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
545DB9EA21511E6300CA77B8 /* Samples.app */,
|
||||
545DB9FE21511E6400CA77B8 /* SamplesTests.xctest */,
|
||||
545DBA0921511E6400CA77B8 /* SamplesUITests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
545DB9EC21511E6300CA77B8 /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
545DB9F421511E6400CA77B8 /* Assets.xcassets */,
|
||||
545DB9F621511E6400CA77B8 /* LaunchScreen.storyboard */,
|
||||
545DB9F121511E6300CA77B8 /* Main.storyboard */,
|
||||
545DB9ED21511E6300CA77B8 /* AppDelegate.swift */,
|
||||
545DB9EF21511E6300CA77B8 /* ViewController.swift */,
|
||||
54B51115216AFE5F0033A6F3 /* UIExtensions.swift */,
|
||||
54CDC5D7215BBE23007D205C /* UIComponents.swift */,
|
||||
545DB9F921511E6400CA77B8 /* Info.plist */,
|
||||
);
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
545DBA0121511E6400CA77B8 /* Tests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
545DBA0221511E6400CA77B8 /* SampleTests.swift */,
|
||||
545DBA0421511E6400CA77B8 /* Info.plist */,
|
||||
);
|
||||
path = Tests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
545DBA0C21511E6400CA77B8 /* UITests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
545DBA0D21511E6400CA77B8 /* SampleUITests.swift */,
|
||||
545DBA0F21511E6400CA77B8 /* Info.plist */,
|
||||
);
|
||||
path = UITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
545DB9E921511E6300CA77B8 /* Samples */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 545DBA1221511E6400CA77B8 /* Build configuration list for PBXNativeTarget "Samples" */;
|
||||
buildPhases = (
|
||||
54D7209621D4DB970054A255 /* ShellScript */,
|
||||
545DB9E621511E6300CA77B8 /* Sources */,
|
||||
545DB9E721511E6300CA77B8 /* Frameworks */,
|
||||
545DB9E821511E6300CA77B8 /* Resources */,
|
||||
549D23CD233C7779008EF4D7 /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Samples;
|
||||
productName = FloatingModalSample;
|
||||
productReference = 545DB9EA21511E6300CA77B8 /* Samples.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
545DB9FD21511E6400CA77B8 /* SamplesTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 545DBA1521511E6400CA77B8 /* Build configuration list for PBXNativeTarget "SamplesTests" */;
|
||||
buildPhases = (
|
||||
545DB9FA21511E6400CA77B8 /* Sources */,
|
||||
545DB9FB21511E6400CA77B8 /* Frameworks */,
|
||||
545DB9FC21511E6400CA77B8 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
545DBA0021511E6400CA77B8 /* PBXTargetDependency */,
|
||||
);
|
||||
name = SamplesTests;
|
||||
productName = FloatingModalSampleTests;
|
||||
productReference = 545DB9FE21511E6400CA77B8 /* SamplesTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
545DBA0821511E6400CA77B8 /* SamplesUITests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 545DBA1821511E6400CA77B8 /* Build configuration list for PBXNativeTarget "SamplesUITests" */;
|
||||
buildPhases = (
|
||||
545DBA0521511E6400CA77B8 /* Sources */,
|
||||
545DBA0621511E6400CA77B8 /* Frameworks */,
|
||||
545DBA0721511E6400CA77B8 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
545DBA0B21511E6400CA77B8 /* PBXTargetDependency */,
|
||||
);
|
||||
name = SamplesUITests;
|
||||
productName = FloatingModalSampleUITests;
|
||||
productReference = 545DBA0921511E6400CA77B8 /* SamplesUITests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.ui-testing";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
545DB9E221511E6300CA77B8 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1000;
|
||||
LastUpgradeCheck = 1000;
|
||||
ORGANIZATIONNAME = scenee;
|
||||
TargetAttributes = {
|
||||
545DB9E921511E6300CA77B8 = {
|
||||
CreatedOnToolsVersion = 10.0;
|
||||
};
|
||||
545DB9FD21511E6400CA77B8 = {
|
||||
CreatedOnToolsVersion = 10.0;
|
||||
TestTargetID = 545DB9E921511E6300CA77B8;
|
||||
};
|
||||
545DBA0821511E6400CA77B8 = {
|
||||
CreatedOnToolsVersion = 10.0;
|
||||
TestTargetID = 545DB9E921511E6300CA77B8;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 545DB9E521511E6300CA77B8 /* Build configuration list for PBXProject "Samples" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 545DB9E121511E6300CA77B8;
|
||||
productRefGroup = 545DB9EB21511E6300CA77B8 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
545DB9E921511E6300CA77B8 /* Samples */,
|
||||
545DB9FD21511E6400CA77B8 /* SamplesTests */,
|
||||
545DBA0821511E6400CA77B8 /* SamplesUITests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
545DB9E821511E6300CA77B8 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
545DB9F821511E6400CA77B8 /* LaunchScreen.storyboard in Resources */,
|
||||
545DB9F521511E6400CA77B8 /* Assets.xcassets in Resources */,
|
||||
545DB9F321511E6300CA77B8 /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
545DB9FC21511E6400CA77B8 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
545DBA0721511E6400CA77B8 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
54D7209621D4DB970054A255 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $(git rev-parse --abbrev-ref HEAD)($(git rev-parse --short HEAD))\" $SRCROOT/$INFOPLIST_FILE\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
545DB9E621511E6300CA77B8 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
54CDC5D8215BBE23007D205C /* UIComponents.swift in Sources */,
|
||||
54B51116216AFE5F0033A6F3 /* UIExtensions.swift in Sources */,
|
||||
545DB9F021511E6300CA77B8 /* ViewController.swift in Sources */,
|
||||
545DB9EE21511E6300CA77B8 /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
545DB9FA21511E6400CA77B8 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
545DBA0321511E6400CA77B8 /* SampleTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
545DBA0521511E6400CA77B8 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
545DBA0E21511E6400CA77B8 /* SampleUITests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
545DBA0021511E6400CA77B8 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 545DB9E921511E6300CA77B8 /* Samples */;
|
||||
targetProxy = 545DB9FF21511E6400CA77B8 /* PBXContainerItemProxy */;
|
||||
};
|
||||
545DBA0B21511E6400CA77B8 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 545DB9E921511E6300CA77B8 /* Samples */;
|
||||
targetProxy = 545DBA0A21511E6400CA77B8 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
545DB9F121511E6300CA77B8 /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
545DB9F221511E6300CA77B8 /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
545DB9F621511E6400CA77B8 /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
545DB9F721511E6400CA77B8 /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
545DBA1021511E6400CA77B8 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
545DBA1121511E6400CA77B8 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
545DBA1321511E6400CA77B8 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = Sources/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelSample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
545DBA1421511E6400CA77B8 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = Sources/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelSample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
545DBA1621511E6400CA77B8 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
INFOPLIST_FILE = Tests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalSampleTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Samples.app/Samples";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
545DBA1721511E6400CA77B8 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
INFOPLIST_FILE = Tests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalSampleTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Samples.app/Samples";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
545DBA1921511E6400CA77B8 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = J3D7L9FHSS;
|
||||
INFOPLIST_FILE = UITests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalSampleUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_TARGET_NAME = FloatingModalSample;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
545DBA1A21511E6400CA77B8 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = J3D7L9FHSS;
|
||||
INFOPLIST_FILE = UITests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalSampleUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_TARGET_NAME = FloatingModalSample;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
545DB9E521511E6300CA77B8 /* Build configuration list for PBXProject "Samples" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
545DBA1021511E6400CA77B8 /* Debug */,
|
||||
545DBA1121511E6400CA77B8 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
545DBA1221511E6400CA77B8 /* Build configuration list for PBXNativeTarget "Samples" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
545DBA1321511E6400CA77B8 /* Debug */,
|
||||
545DBA1421511E6400CA77B8 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
545DBA1521511E6400CA77B8 /* Build configuration list for PBXNativeTarget "SamplesTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
545DBA1621511E6400CA77B8 /* Debug */,
|
||||
545DBA1721511E6400CA77B8 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
545DBA1821511E6400CA77B8 /* Build configuration list for PBXNativeTarget "SamplesUITests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
545DBA1921511E6400CA77B8 /* Debug */,
|
||||
545DBA1A21511E6400CA77B8 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 545DB9E221511E6300CA77B8 /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1000"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "545DB9E921511E6300CA77B8"
|
||||
BuildableName = "Samples.app"
|
||||
BlueprintName = "Samples"
|
||||
ReferencedContainer = "container:Samples.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "545DB9E921511E6300CA77B8"
|
||||
BuildableName = "Samples.app"
|
||||
BlueprintName = "Samples"
|
||||
ReferencedContainer = "container:Samples.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "545DB9E921511E6300CA77B8"
|
||||
BuildableName = "Samples.app"
|
||||
BlueprintName = "Samples"
|
||||
ReferencedContainer = "container:Samples.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "545DB9E921511E6300CA77B8"
|
||||
BuildableName = "Samples.app"
|
||||
BlueprintName = "Samples"
|
||||
ReferencedContainer = "container:Samples.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,11 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/09/18.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "60x60",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "60x60",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "29x29",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "29x29",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "40x40",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "40x40",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "76x76",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "76x76",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "83.5x83.5",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"size" : "1024x1024",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14283.14"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,715 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
|
||||
<device id="retina5_9" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="Cjh-iX-VQw">
|
||||
<objects>
|
||||
<navigationController id="RoN-h0-uBD" sceneMemberID="viewController">
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="hNW-5m-Omi">
|
||||
<rect key="frame" x="0.0" y="44" width="375" height="96"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="jF4-A0-Eq6" kind="relationship" relationship="rootViewController" id="W9V-or-flQ"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="lKu-or-aPl" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-705" y="27"/>
|
||||
</scene>
|
||||
<!--Samples-->
|
||||
<scene sceneID="35L-Gs-Vts">
|
||||
<objects>
|
||||
<viewController id="jF4-A0-Eq6" customClass="SampleListViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Smh-Bd-AAc">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="7IS-PU-x0P">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="M0G-C8-hAO" style="IBUITableViewCellStyleDefault" id="ySY-oA-g81">
|
||||
<rect key="frame" x="0.0" y="28" width="375" height="43.666667938232422"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ySY-oA-g81" id="sXB-nH-2g2">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="43.666667938232422"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="M0G-C8-hAO">
|
||||
<rect key="frame" x="15" y="0.0" width="345" height="43.666667938232422"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
</tableView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="7IS-PU-x0P" firstAttribute="top" secondItem="Smh-Bd-AAc" secondAttribute="top" id="6yd-jv-ey3"/>
|
||||
<constraint firstItem="7IS-PU-x0P" firstAttribute="leading" secondItem="39L-Nq-qfp" secondAttribute="leading" id="Z6Y-Dc-cei"/>
|
||||
<constraint firstItem="7IS-PU-x0P" firstAttribute="bottom" secondItem="Smh-Bd-AAc" secondAttribute="bottom" id="fNW-DP-lhV"/>
|
||||
<constraint firstItem="7IS-PU-x0P" firstAttribute="trailing" secondItem="39L-Nq-qfp" secondAttribute="trailing" id="vfY-Rc-FOI"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="39L-Nq-qfp"/>
|
||||
</view>
|
||||
<navigationItem key="navigationItem" title="Samples" id="wCF-su-7up">
|
||||
<barButtonItem key="rightBarButtonItem" title="Settings" id="rbH-U3-XyA">
|
||||
<connections>
|
||||
<action selector="showDebugMenu:" destination="jF4-A0-Eq6" id="j02-db-ZM5"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="tableView" destination="7IS-PU-x0P" id="YFM-9W-eP4"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="eP2-DG-flv" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="57" y="27"/>
|
||||
</scene>
|
||||
<!--Settings View Controller-->
|
||||
<scene sceneID="Bd0-D2-agO">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="SettingsViewController" id="C1X-9Z-TyQ" customClass="SettingsViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="af9-Zr-Ppc">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="197.33333333333334"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="n93-ZL-fmC">
|
||||
<rect key="frame" x="32" y="16" width="311" height="181.33333333333334"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Version: 1.0" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WmC-Tq-NDN">
|
||||
<rect key="frame" x="118.33333333333334" y="0.0" width="74.333333333333343" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="UINavigationBar" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ulg-gS-ah0">
|
||||
<rect key="frame" x="90.666666666666686" y="37" width="130" height="25"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="126" translatesAutoresizingMaskIntoConstraints="NO" id="uEf-g4-CeU">
|
||||
<rect key="frame" x="23.333333333333343" y="78" width="264.66666666666663" height="38"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Large Titles" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ogl-S5-4tJ">
|
||||
<rect key="frame" x="0.0" y="8.9999999999999982" width="89.666666666666671" height="20.333333333333329"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="js8-Qv-lUC">
|
||||
<rect key="frame" x="215.66666666666666" y="3.6666666666666714" width="50.999999999999972" height="31"/>
|
||||
<connections>
|
||||
<action selector="toggleLargeTitle:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="FJS-Ty-mCY"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="126" translatesAutoresizingMaskIntoConstraints="NO" id="ZtZ-Dz-4cC">
|
||||
<rect key="frame" x="23.333333333333343" y="132" width="264.66666666666663" height="49.333333333333343"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Translucent" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Z5i-rm-QgL">
|
||||
<rect key="frame" x="0.0" y="0.0" width="89.666666666666671" height="49.333333333333336"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="s6b-j9-8Kw">
|
||||
<rect key="frame" x="215.66666666666666" y="0.0" width="50.999999999999972" height="49.333333333333336"/>
|
||||
<connections>
|
||||
<action selector="toggleTranslucent:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="nL4-3L-9hh"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="0hr-ty-yWm" firstAttribute="bottom" secondItem="n93-ZL-fmC" secondAttribute="bottom" id="2Ey-ou-E1M"/>
|
||||
<constraint firstAttribute="trailing" secondItem="n93-ZL-fmC" secondAttribute="trailing" constant="32" id="DdZ-eB-F5s"/>
|
||||
<constraint firstItem="n93-ZL-fmC" firstAttribute="leading" secondItem="af9-Zr-Ppc" secondAttribute="leading" constant="32" id="TyK-GP-Ari"/>
|
||||
<constraint firstItem="n93-ZL-fmC" firstAttribute="top" secondItem="af9-Zr-Ppc" secondAttribute="topMargin" constant="16" id="mbC-6H-z9M"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="0hr-ty-yWm"/>
|
||||
</view>
|
||||
<size key="freeformSize" width="375" height="197.33000000000001"/>
|
||||
<connections>
|
||||
<outlet property="largeTitlesSwicth" destination="js8-Qv-lUC" id="FOm-6k-ffi"/>
|
||||
<outlet property="translucentSwicth" destination="s6b-j9-8Kw" id="jmf-WH-bzZ"/>
|
||||
<outlet property="versionLabel" destination="WmC-Tq-NDN" id="Woh-kK-U0m"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="M9h-4V-3M0" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="708" y="-200"/>
|
||||
</scene>
|
||||
<!--Layout 2-->
|
||||
<scene sceneID="lRc-OZ-sL4">
|
||||
<objects>
|
||||
<viewController id="RpE-lI-27a" customClass="TabBarContentViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="JER-jz-KSq">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="IvG-yp-yzI">
|
||||
<rect key="frame" x="20" y="44" width="39" height="30"/>
|
||||
<state key="normal" title="Close"/>
|
||||
<connections>
|
||||
<action selector="closeWithSender:" destination="RpE-lI-27a" eventType="touchUpInside" id="hj3-Xv-6Gq"/>
|
||||
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="rg4-OH-Ojn"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="IvG-yp-yzI" firstAttribute="top" secondItem="954-Dk-zvc" secondAttribute="top" id="18k-sV-PgT"/>
|
||||
<constraint firstItem="954-Dk-zvc" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="IvG-yp-yzI" secondAttribute="trailing" id="mpr-u5-MZu"/>
|
||||
<constraint firstItem="IvG-yp-yzI" firstAttribute="leading" secondItem="954-Dk-zvc" secondAttribute="leading" constant="20" id="pYt-jE-CTF"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="954-Dk-zvc"/>
|
||||
</view>
|
||||
<tabBarItem key="tabBarItem" tag="1" title="Layout 2" id="qb3-RB-B28"/>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="NhZ-u5-Beh" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-308" y="1546"/>
|
||||
</scene>
|
||||
<!--Layout 3-->
|
||||
<scene sceneID="r9h-Ql-gIv">
|
||||
<objects>
|
||||
<viewController id="pOk-Zm-vD9" customClass="TabBarContentViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="85d-ub-G8k">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NbG-e8-HdI">
|
||||
<rect key="frame" x="20" y="44" width="39" height="30"/>
|
||||
<state key="normal" title="Close"/>
|
||||
<connections>
|
||||
<action selector="closeWithSender:" destination="pOk-Zm-vD9" eventType="touchUpInside" id="111-PD-Pop"/>
|
||||
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="1Rg-YG-TtU"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="0ao-SI-QZW" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="NbG-e8-HdI" secondAttribute="trailing" id="K9F-6x-KWn"/>
|
||||
<constraint firstItem="NbG-e8-HdI" firstAttribute="top" secondItem="0ao-SI-QZW" secondAttribute="top" id="nsE-so-rTl"/>
|
||||
<constraint firstItem="NbG-e8-HdI" firstAttribute="leading" secondItem="0ao-SI-QZW" secondAttribute="leading" constant="20" id="sF4-Dm-aoY"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="0ao-SI-QZW"/>
|
||||
</view>
|
||||
<tabBarItem key="tabBarItem" tag="2" title="Layout 3" id="RJD-TF-Sdh"/>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Oe3-FT-q1C" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="332" y="1546"/>
|
||||
</scene>
|
||||
<!--Layout 1-->
|
||||
<scene sceneID="m6X-j6-yBM">
|
||||
<objects>
|
||||
<viewController id="lto-Zc-Vtp" customClass="TabBarContentViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="ji9-Ez-N7i">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eFN-tN-4Ct">
|
||||
<rect key="frame" x="20" y="44" width="39" height="30"/>
|
||||
<state key="normal" title="Close"/>
|
||||
<connections>
|
||||
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="YL4-GP-ZEZ"/>
|
||||
<action selector="closeWithSender:" destination="lto-Zc-Vtp" eventType="touchUpInside" id="llo-9x-fQv"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="eFN-tN-4Ct" firstAttribute="leading" secondItem="5Ns-4l-Ufg" secondAttribute="leading" constant="20" id="5BT-yZ-EKe"/>
|
||||
<constraint firstItem="5Ns-4l-Ufg" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="eFN-tN-4Ct" secondAttribute="trailing" id="OzZ-Dz-RNF"/>
|
||||
<constraint firstItem="eFN-tN-4Ct" firstAttribute="top" secondItem="5Ns-4l-Ufg" secondAttribute="top" id="hUV-3a-XkY"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="5Ns-4l-Ufg"/>
|
||||
</view>
|
||||
<tabBarItem key="tabBarItem" title="Layout 1" id="HEV-kf-jxH"/>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="bkL-bc-hZC" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-962" y="1546"/>
|
||||
</scene>
|
||||
<!--Intrinsic View Controller-->
|
||||
<scene sceneID="wtJ-qZ-aCl">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="IntrinsicViewController" title="Intrinsic View Controller" id="aK0-kv-mTu" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="eLM-xc-d9e">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="1000" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" verticalCompressionResistancePriority="1000" text="Change this text" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ge4-RW-Gmz">
|
||||
<rect key="frame" x="24" y="24" width="327" height="20.333333333333329"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="ge4-RW-Gmz" secondAttribute="trailing" constant="24" id="V59-MD-Lcg"/>
|
||||
<constraint firstItem="ge4-RW-Gmz" firstAttribute="leading" secondItem="eLM-xc-d9e" secondAttribute="leading" constant="24" id="hAO-P0-7Kw"/>
|
||||
<constraint firstItem="ge4-RW-Gmz" firstAttribute="top" secondItem="eLM-xc-d9e" secondAttribute="top" constant="24" id="j0s-fd-MYj"/>
|
||||
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="ge4-RW-Gmz" secondAttribute="bottom" constant="24" id="tEn-PO-nVD"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="ouu-g9-OiX"/>
|
||||
</view>
|
||||
<size key="freeformSize" width="375" height="778"/>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="DfE-fL-zy5" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2753" y="734"/>
|
||||
</scene>
|
||||
<!--Tab Bar View Controller-->
|
||||
<scene sceneID="nQ5-PV-qFw">
|
||||
<objects>
|
||||
<tabBarController storyboardIdentifier="TabBarViewController" id="c7K-XJ-TlT" customClass="TabBarViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tabBar key="tabBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="BPL-Dp-5pJ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="49"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</tabBar>
|
||||
<connections>
|
||||
<segue destination="lto-Zc-Vtp" kind="relationship" relationship="viewControllers" id="6hP-AH-YiH"/>
|
||||
<segue destination="RpE-lI-27a" kind="relationship" relationship="viewControllers" id="g6X-Sq-uSW"/>
|
||||
<segue destination="pOk-Zm-vD9" kind="relationship" relationship="viewControllers" id="OPp-iO-iDK"/>
|
||||
</connections>
|
||||
</tabBarController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Z9x-EI-p2b" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-706" y="749"/>
|
||||
</scene>
|
||||
<!--Modal View Controller-->
|
||||
<scene sceneID="C9P-Ns-Qrq">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="ModalViewController" id="bYI-y3-Rzb" customClass="ModalViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="qwo-GK-p1U">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="724"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="vut-mK-Y4t" customClass="SafeAreaView" customModule="Samples" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="724" width="375" height="0.0"/>
|
||||
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</view>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="sbF-Az-7sy">
|
||||
<rect key="frame" x="20" y="0.0" width="39" height="30"/>
|
||||
<state key="normal" title="Close"/>
|
||||
<connections>
|
||||
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="MSC-ch-YJK"/>
|
||||
</connections>
|
||||
</button>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="44" translatesAutoresizingMaskIntoConstraints="NO" id="9p4-06-y2T">
|
||||
<rect key="frame" x="134.66666666666666" y="88" width="106" height="326"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="i9x-x5-n1q">
|
||||
<rect key="frame" x="0.0" y="0.0" width="80" height="30"/>
|
||||
<state key="normal" title="Move to full"/>
|
||||
<connections>
|
||||
<action selector="moveToFullWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="TDe-3J-gIR"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2u5-cH-RAN">
|
||||
<rect key="frame" x="0.0" y="74" width="85" height="30"/>
|
||||
<state key="normal" title="Move to half"/>
|
||||
<connections>
|
||||
<action selector="moveToHalfWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="12s-o7-Et5"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="M4A-iO-RIE">
|
||||
<rect key="frame" x="0.0" y="148" width="77" height="30"/>
|
||||
<state key="normal" title="Move to tip"/>
|
||||
<connections>
|
||||
<action selector="moveToTipWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="BmL-91-9ai"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="swr-XM-GzZ">
|
||||
<rect key="frame" x="0.0" y="222" width="106" height="30"/>
|
||||
<state key="normal" title="Move to hidden"/>
|
||||
<connections>
|
||||
<action selector="moveToHiddenWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="jfJ-0f-fdk"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="szf-HE-QTk">
|
||||
<rect key="frame" x="0.0" y="296" width="96" height="30"/>
|
||||
<state key="normal" title="Update layout"/>
|
||||
<connections>
|
||||
<action selector="updateLayout:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="Woz-a7-YMJ"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="sbF-Az-7sy" firstAttribute="top" secondItem="kjr-TP-fcM" secondAttribute="top" id="3VR-hj-zeQ"/>
|
||||
<constraint firstItem="9p4-06-y2T" firstAttribute="top" secondItem="kjr-TP-fcM" secondAttribute="top" constant="88" id="41n-Fn-hi3"/>
|
||||
<constraint firstAttribute="bottom" secondItem="vut-mK-Y4t" secondAttribute="bottom" id="6eq-Kt-heZ"/>
|
||||
<constraint firstItem="sbF-Az-7sy" firstAttribute="leading" secondItem="kjr-TP-fcM" secondAttribute="leading" constant="20" id="T2G-1L-PRs"/>
|
||||
<constraint firstItem="vut-mK-Y4t" firstAttribute="leading" secondItem="kjr-TP-fcM" secondAttribute="leading" id="gVC-jv-VJX"/>
|
||||
<constraint firstItem="vut-mK-Y4t" firstAttribute="trailing" secondItem="kjr-TP-fcM" secondAttribute="trailing" id="jkq-p2-lUm"/>
|
||||
<constraint firstItem="9p4-06-y2T" firstAttribute="centerX" secondItem="kjr-TP-fcM" secondAttribute="centerX" id="l8t-p3-ETf"/>
|
||||
<constraint firstItem="vut-mK-Y4t" firstAttribute="top" secondItem="kjr-TP-fcM" secondAttribute="bottom" id="rMy-JT-t4B"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="kjr-TP-fcM"/>
|
||||
</view>
|
||||
<size key="freeformSize" width="375" height="778"/>
|
||||
<connections>
|
||||
<outlet property="safeAreaView" destination="vut-mK-Y4t" id="r9P-XF-wLd"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="fbi-LZ-M4Y" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1375" y="734"/>
|
||||
</scene>
|
||||
<!--Nested Scroll View Controller-->
|
||||
<scene sceneID="TfC-A3-4R0">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="NestedScrollViewController" id="LAe-jm-k6f" customClass="NestedScrollViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="414-Wy-0t1">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="sBe-tN-uMi">
|
||||
<rect key="frame" x="0.0" y="32" width="375" height="746"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="lFR-Sp-Sj1">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="968"/>
|
||||
<subviews>
|
||||
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceHorizontal="YES" pagingEnabled="YES" showsVerticalScrollIndicator="NO" bouncesZoom="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xba-kG-VQ2">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="242"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="WRe-tD-KTb">
|
||||
<rect key="frame" x="0.0" y="0.0" width="1125" height="242"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="WuE-iq-0GW">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="242"/>
|
||||
<color key="backgroundColor" red="1" green="0.57810515169999999" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="w5Y-44-79g">
|
||||
<rect key="frame" x="375" y="0.0" width="375" height="242"/>
|
||||
<color key="backgroundColor" red="0.0078431372550000003" green="0.72156862749999995" blue="0.45882352939999999" alpha="1" colorSpace="calibratedRGB"/>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="jQf-k3-eAa">
|
||||
<rect key="frame" x="750" y="0.0" width="375" height="242"/>
|
||||
<color key="backgroundColor" red="0.016804177310000001" green="0.19835099580000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="w5Y-44-79g" firstAttribute="width" secondItem="WuE-iq-0GW" secondAttribute="width" id="kHy-eU-guw"/>
|
||||
<constraint firstAttribute="height" constant="242" id="qzg-fI-j20"/>
|
||||
<constraint firstItem="jQf-k3-eAa" firstAttribute="width" secondItem="WuE-iq-0GW" secondAttribute="width" id="zDe-Uj-FO0"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="WRe-tD-KTb" firstAttribute="leading" secondItem="xba-kG-VQ2" secondAttribute="leading" id="7QG-dB-afb"/>
|
||||
<constraint firstAttribute="height" constant="242" id="Efw-D6-ksg"/>
|
||||
<constraint firstAttribute="trailing" secondItem="WRe-tD-KTb" secondAttribute="trailing" id="ReM-cV-k0J"/>
|
||||
<constraint firstItem="WRe-tD-KTb" firstAttribute="top" secondItem="xba-kG-VQ2" secondAttribute="top" id="Xla-QL-qwm"/>
|
||||
<constraint firstItem="WuE-iq-0GW" firstAttribute="width" secondItem="xba-kG-VQ2" secondAttribute="width" id="qm0-cd-P69"/>
|
||||
<constraint firstAttribute="bottom" secondItem="WRe-tD-KTb" secondAttribute="bottom" id="uha-Eo-lsv"/>
|
||||
</constraints>
|
||||
</scrollView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="j8d-Tc-XQn">
|
||||
<rect key="frame" x="0.0" y="242" width="375" height="242"/>
|
||||
<color key="backgroundColor" red="0.0078431372550000003" green="0.72156862749999995" blue="0.45882352939999999" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="242" id="Kw8-aw-DIp"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bxy-HF-a7J">
|
||||
<rect key="frame" x="0.0" y="484" width="375" height="242"/>
|
||||
<color key="backgroundColor" red="1" green="0.2527923882" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="242" id="AIb-xl-srX"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ZzA-fs-Va5">
|
||||
<rect key="frame" x="0.0" y="726" width="375" height="242"/>
|
||||
<color key="backgroundColor" red="0.016804177310000001" green="0.19835099580000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="242" id="TC1-jO-Wcz"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="lFR-Sp-Sj1" firstAttribute="width" secondItem="sBe-tN-uMi" secondAttribute="width" id="AtD-2C-97K"/>
|
||||
<constraint firstAttribute="trailing" secondItem="lFR-Sp-Sj1" secondAttribute="trailing" id="F7t-Kr-VGd"/>
|
||||
<constraint firstItem="lFR-Sp-Sj1" firstAttribute="leading" secondItem="sBe-tN-uMi" secondAttribute="leading" id="LzI-O9-5i0"/>
|
||||
<constraint firstItem="lFR-Sp-Sj1" firstAttribute="top" secondItem="sBe-tN-uMi" secondAttribute="top" id="VwX-Hz-e8V"/>
|
||||
<constraint firstAttribute="bottom" secondItem="lFR-Sp-Sj1" secondAttribute="bottom" id="hJt-0Z-dF3"/>
|
||||
</constraints>
|
||||
</scrollView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<gestureRecognizers/>
|
||||
<constraints>
|
||||
<constraint firstItem="sBe-tN-uMi" firstAttribute="leading" secondItem="ufS-Rf-F2F" secondAttribute="leading" id="8Qd-my-knA"/>
|
||||
<constraint firstItem="sBe-tN-uMi" firstAttribute="top" secondItem="414-Wy-0t1" secondAttribute="top" constant="32" id="9Js-LU-lNr"/>
|
||||
<constraint firstAttribute="bottom" secondItem="sBe-tN-uMi" secondAttribute="bottom" id="jzB-47-P7e"/>
|
||||
<constraint firstItem="ufS-Rf-F2F" firstAttribute="trailing" secondItem="sBe-tN-uMi" secondAttribute="trailing" id="nHG-wg-pLP"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="ufS-Rf-F2F"/>
|
||||
<connections>
|
||||
<outletCollection property="gestureRecognizers" destination="tOa-bf-zGz" appends="YES" id="zle-Sz-M3U"/>
|
||||
<outletCollection property="gestureRecognizers" destination="SCk-hG-weZ" appends="YES" id="OcK-FK-Lac"/>
|
||||
<outletCollection property="gestureRecognizers" destination="Fvp-Z6-eVc" appends="YES" id="Fds-J5-YCg"/>
|
||||
</connections>
|
||||
</view>
|
||||
<size key="freeformSize" width="375" height="778"/>
|
||||
<connections>
|
||||
<outlet property="nestedScrollView" destination="xba-kG-VQ2" id="ddV-kf-37A"/>
|
||||
<outlet property="scrollView" destination="sBe-tN-uMi" id="h4S-Zl-cLO"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="QSd-gF-l5h" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<pongPressGestureRecognizer allowableMovement="10" minimumPressDuration="0.5" id="tOa-bf-zGz">
|
||||
<connections>
|
||||
<action selector="longPressed:" destination="LAe-jm-k6f" id="sE8-3l-Aos"/>
|
||||
</connections>
|
||||
</pongPressGestureRecognizer>
|
||||
<tapGestureRecognizer id="SCk-hG-weZ">
|
||||
<connections>
|
||||
<action selector="tapped:" destination="LAe-jm-k6f" id="0Cw-vR-zRP"/>
|
||||
</connections>
|
||||
</tapGestureRecognizer>
|
||||
<swipeGestureRecognizer direction="right" id="Fvp-Z6-eVc">
|
||||
<connections>
|
||||
<action selector="swipped:" destination="LAe-jm-k6f" id="Hav-7p-Tg8"/>
|
||||
</connections>
|
||||
</swipeGestureRecognizer>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2097" y="734"/>
|
||||
</scene>
|
||||
<!--Detail View Controller-->
|
||||
<scene sceneID="b6k-zi-3wn">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="DetailViewController" id="YC8-ae-15L" customClass="DetailViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="g7l-kO-y7q">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="8yw-OC-Ubk">
|
||||
<rect key="frame" x="0.0" y="690" width="375" height="88"/>
|
||||
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="88" id="jwV-YU-tXG"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view alpha="0.5" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Kva-Z7-0qY" customClass="OnSafeAreaView" customModule="Samples" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="44" width="375" height="734"/>
|
||||
<color key="backgroundColor" red="0.0078431372550000003" green="0.72156862749999995" blue="0.45882352939999999" alpha="1" colorSpace="calibratedRGB"/>
|
||||
</view>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="noi-1a-5bZ" customClass="CloseButton" customModule="Samples" customModuleProvider="target">
|
||||
<rect key="frame" x="319" y="44" width="44" height="44"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="44" id="0jg-5D-A1F"/>
|
||||
<constraint firstAttribute="width" constant="44" id="1Cq-PA-wgW"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<action selector="closeWithSender:" destination="YC8-ae-15L" eventType="touchUpInside" id="Z2v-19-S5k"/>
|
||||
</connections>
|
||||
</button>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="qux-uG-4o2">
|
||||
<rect key="frame" x="8" y="52" width="148.33333333333334" height="31"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="fitToBounds" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7lq-d3-PKi">
|
||||
<rect key="frame" x="0.0" y="5.3333333333333357" width="91.333333333333329" height="20.333333333333332"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0MA-lV-KjS">
|
||||
<rect key="frame" x="99.333333333333329" y="0.0" width="50.999999999999986" height="31"/>
|
||||
<connections>
|
||||
<action selector="modeChanged:" destination="YC8-ae-15L" eventType="valueChanged" id="IQ8-u2-Rib"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="22" translatesAutoresizingMaskIntoConstraints="NO" id="tP3-oJ-4EB">
|
||||
<rect key="frame" x="130.66666666666666" y="132" width="114" height="134"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="c5r-jU-haj">
|
||||
<rect key="frame" x="0.0" y="0.0" width="114" height="30"/>
|
||||
<state key="normal" title="Show"/>
|
||||
<connections>
|
||||
<action selector="buttonPressed:" destination="YC8-ae-15L" eventType="touchUpInside" id="Mi1-o6-TWt"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="wmd-ab-Nz3">
|
||||
<rect key="frame" x="0.0" y="52" width="114" height="30"/>
|
||||
<state key="normal" title="Present Modallly"/>
|
||||
<connections>
|
||||
<action selector="buttonPressed:" destination="YC8-ae-15L" eventType="touchUpInside" id="tjH-Ev-kpx"/>
|
||||
<segue destination="bYI-y3-Rzb" kind="presentation" identifier="PresentModallySegue" id="3yq-HE-Tgn"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="01L-lp-oy6">
|
||||
<rect key="frame" x="0.0" y="104" width="114" height="30"/>
|
||||
<state key="normal" title="Update Layout"/>
|
||||
<connections>
|
||||
<action selector="buttonPressed:" destination="YC8-ae-15L" eventType="touchUpInside" id="zTb-sq-B6f"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<gestureRecognizers/>
|
||||
<constraints>
|
||||
<constraint firstItem="noi-1a-5bZ" firstAttribute="top" secondItem="aOK-7l-cA6" secondAttribute="top" id="EQy-cr-F2Y"/>
|
||||
<constraint firstItem="tP3-oJ-4EB" firstAttribute="centerX" secondItem="aOK-7l-cA6" secondAttribute="centerX" id="EsD-Vf-dNZ"/>
|
||||
<constraint firstItem="Kva-Z7-0qY" firstAttribute="top" secondItem="aOK-7l-cA6" secondAttribute="top" id="Fff-HL-4mo"/>
|
||||
<constraint firstItem="8yw-OC-Ubk" firstAttribute="bottom" secondItem="g7l-kO-y7q" secondAttribute="bottom" id="JOL-wC-w74"/>
|
||||
<constraint firstItem="8yw-OC-Ubk" firstAttribute="leading" secondItem="aOK-7l-cA6" secondAttribute="leading" id="RiJ-Hb-OOZ"/>
|
||||
<constraint firstItem="8yw-OC-Ubk" firstAttribute="trailing" secondItem="aOK-7l-cA6" secondAttribute="trailing" id="Sof-yL-mwK"/>
|
||||
<constraint firstItem="tP3-oJ-4EB" firstAttribute="top" secondItem="aOK-7l-cA6" secondAttribute="top" constant="88" id="Zhb-Ss-epe"/>
|
||||
<constraint firstItem="Kva-Z7-0qY" firstAttribute="trailing" secondItem="aOK-7l-cA6" secondAttribute="trailing" id="kkp-Yo-FQW"/>
|
||||
<constraint firstItem="aOK-7l-cA6" firstAttribute="trailing" secondItem="noi-1a-5bZ" secondAttribute="trailing" constant="12" id="lv9-Nf-HNB"/>
|
||||
<constraint firstItem="qux-uG-4o2" firstAttribute="top" secondItem="aOK-7l-cA6" secondAttribute="top" constant="8" id="naa-cf-ZIc"/>
|
||||
<constraint firstItem="Kva-Z7-0qY" firstAttribute="leading" secondItem="aOK-7l-cA6" secondAttribute="leading" id="oVC-i1-TwS"/>
|
||||
<constraint firstItem="aOK-7l-cA6" firstAttribute="bottom" secondItem="Kva-Z7-0qY" secondAttribute="bottom" id="rW2-mF-5DR"/>
|
||||
<constraint firstItem="8yw-OC-Ubk" firstAttribute="top" relation="greaterThanOrEqual" secondItem="tP3-oJ-4EB" secondAttribute="bottom" constant="88" id="vKQ-h9-uKt"/>
|
||||
<constraint firstItem="qux-uG-4o2" firstAttribute="leading" secondItem="g7l-kO-y7q" secondAttribute="leading" constant="8" id="zXb-R9-bMO"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="aOK-7l-cA6"/>
|
||||
<connections>
|
||||
<outletCollection property="gestureRecognizers" destination="6Ca-p8-7uF" appends="YES" id="xOy-f1-NZE"/>
|
||||
<outletCollection property="gestureRecognizers" destination="SPY-Vr-XDT" appends="YES" id="vgS-Am-jhQ"/>
|
||||
<outletCollection property="gestureRecognizers" destination="Jg4-it-qJ5" appends="YES" id="ONf-5y-phY"/>
|
||||
</connections>
|
||||
</view>
|
||||
<size key="freeformSize" width="375" height="778"/>
|
||||
<connections>
|
||||
<outlet property="closeButton" destination="noi-1a-5bZ" id="eWQ-ha-8y7"/>
|
||||
<outlet property="intrinsicHeightConstraint" destination="vKQ-h9-uKt" id="QpA-WD-b17"/>
|
||||
<outlet property="modeChangeView" destination="qux-uG-4o2" id="1Nq-fE-dXw"/>
|
||||
<segue destination="bYI-y3-Rzb" kind="show" identifier="ShowSegue" id="r1P-2i-NDe"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Wqk-xl-O3I" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<tapGestureRecognizer id="6Ca-p8-7uF">
|
||||
<connections>
|
||||
<action selector="tapped:" destination="YC8-ae-15L" id="KFd-eT-RLn"/>
|
||||
</connections>
|
||||
</tapGestureRecognizer>
|
||||
<swipeGestureRecognizer direction="right" id="SPY-Vr-XDT">
|
||||
<connections>
|
||||
<action selector="swipped:" destination="YC8-ae-15L" id="OFa-4C-8rI"/>
|
||||
</connections>
|
||||
</swipeGestureRecognizer>
|
||||
<pongPressGestureRecognizer allowableMovement="10" minimumPressDuration="0.5" id="Jg4-it-qJ5">
|
||||
<connections>
|
||||
<action selector="longPressed:" destination="YC8-ae-15L" id="1G4-cf-RDE"/>
|
||||
</connections>
|
||||
</pongPressGestureRecognizer>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="653.60000000000002" y="733.74384236453204"/>
|
||||
</scene>
|
||||
<!--Debug Text View Controller-->
|
||||
<scene sceneID="Bkq-O7-q4A">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="ConsoleViewController" id="tvD-nO-QUb" customClass="DebugTextViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="9YG-0j-Zzg">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" bouncesZoom="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rN1-HL-YHv">
|
||||
<rect key="frame" x="0.0" y="17" width="375" height="761"/>
|
||||
<color key="backgroundColor" white="1" alpha="0.5" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<string key="text">The standard Lorem Ipsum passage, used since the 1500s
|
||||
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
|
||||
|
||||
Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
|
||||
|
||||
"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?"
|
||||
|
||||
1914 translation by H. Rackham
|
||||
|
||||
"But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?"
|
||||
|
||||
Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
|
||||
|
||||
"At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat."
|
||||
|
||||
1914 translation by H. Rackham
|
||||
|
||||
"On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee the pain and trouble that are bound to ensue; and equal blame belongs to those who fail in their duty through weakness of will, which is the same as saying through shrinking from toil and pain. These cases are perfectly simple and easy to distinguish. In a free hour, when our power of choice is untrammelled and when nothing prevents our being able to do what we like best, every pleasure is to be welcomed and every pain avoided. But in certain circumstances and owing to the claims of duty or the obligations of business it will frequently occur that pleasures have to be repudiated and annoyances accepted. The wise man therefore always holds in these matters to this principle of selection: he rejects pleasures to secure other greater pleasures, or else he endures pains to avoid worse pains."
|
||||
|
||||
The standard Lorem Ipsum passage, used since the 1500s
|
||||
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
|
||||
|
||||
Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
|
||||
|
||||
"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?"
|
||||
|
||||
1914 translation by H. Rackham
|
||||
|
||||
"But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?"
|
||||
|
||||
Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
|
||||
|
||||
"At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat."
|
||||
|
||||
1914 translation by H. Rackham
|
||||
|
||||
"On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee the pain and trouble that are bound to ensue; and equal blame belongs to those who fail in their duty through weakness of will, which is the same as saying through shrinking from toil and pain. These cases are perfectly simple and easy to distinguish. In a free hour, when our power of choice is untrammelled and when nothing prevents our being able to do what we like best, every pleasure is to be welcomed and every pain avoided. But in certain circumstances and owing to the claims of duty or the obligations of business it will frequently occur that pleasures have to be repudiated and annoyances accepted. The wise man therefore always holds in these matters to this principle of selection: he rejects pleasures to secure other greater pleasures, or else he endures pains to avoid worse pains."</string>
|
||||
<fontDescription key="fontDescription" name="CourierNewPSMT" family="Courier New" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="rN1-HL-YHv" firstAttribute="leading" secondItem="5ET-zC-lCb" secondAttribute="leading" id="7V3-KL-vXd"/>
|
||||
<constraint firstAttribute="bottom" secondItem="rN1-HL-YHv" secondAttribute="bottom" id="efD-U5-Tet"/>
|
||||
<constraint firstItem="rN1-HL-YHv" firstAttribute="top" secondItem="9YG-0j-Zzg" secondAttribute="top" constant="17" id="fiO-LL-nSC"/>
|
||||
<constraint firstItem="rN1-HL-YHv" firstAttribute="trailing" secondItem="5ET-zC-lCb" secondAttribute="trailing" id="lfg-EE-euw"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="5ET-zC-lCb"/>
|
||||
</view>
|
||||
<size key="freeformSize" width="375" height="778"/>
|
||||
<connections>
|
||||
<outlet property="textView" destination="rN1-HL-YHv" id="gmr-Uf-jd8"/>
|
||||
<outlet property="textViewTopConstraint" destination="fiO-LL-nSC" id="Rum-TN-c2e"/>
|
||||
<outlet property="view" destination="9YG-0j-Zzg" id="jhb-eT-nEn"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="x1h-y1-h8q" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-1" y="734"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="r1P-2i-NDe"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
</document>
|
||||
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,103 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/09/19.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@IBDesignable
|
||||
class CloseButton: UIButton {
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
render()
|
||||
}
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
render()
|
||||
}
|
||||
|
||||
func render() {
|
||||
self.backgroundColor = .clear
|
||||
}
|
||||
|
||||
func p(_ p: CGFloat) -> CGFloat {
|
||||
return p * (2.0 / 3.0)
|
||||
}
|
||||
|
||||
override var isHighlighted: Bool { didSet { setNeedsDisplay() } }
|
||||
override var isSelected: Bool { didSet { setNeedsDisplay() } }
|
||||
|
||||
override func draw(_ rect: CGRect) {
|
||||
guard let context = UIGraphicsGetCurrentContext() else { return }
|
||||
|
||||
context.setLineWidth(p(1.0))
|
||||
|
||||
let color = UIColor(displayP3Red: 0.76,
|
||||
green: 0.77,
|
||||
blue: 0.76,
|
||||
alpha: 1.0)
|
||||
|
||||
context.setFillColor(color.cgColor)
|
||||
|
||||
context.beginPath()
|
||||
context.addArc(center: CGPoint(x: rect.width * 0.5,
|
||||
y: rect.height * 0.5),
|
||||
radius: p(36.0) * 0.5,
|
||||
startAngle: 0,
|
||||
endAngle: CGFloat.pi * 2.0,
|
||||
clockwise: true)
|
||||
context.fillPath()
|
||||
|
||||
let highlightedColor = UIColor(displayP3Red: 0.53,
|
||||
green: 0.53,
|
||||
blue: 0.53,
|
||||
alpha: 1.0)
|
||||
|
||||
let crossColor: UIColor = isHighlighted || isSelected ? highlightedColor : .white
|
||||
context.setStrokeColor(crossColor.cgColor)
|
||||
context.setBlendMode(.normal)
|
||||
context.setLineWidth(p(3.5))
|
||||
context.setLineCap(.round)
|
||||
|
||||
let offset = (rect.width - p(36.0)) * 0.5
|
||||
|
||||
context.beginPath()
|
||||
context.addLines(between: [CGPoint(x: offset + p(12.0), y: offset + p(12.0)),
|
||||
CGPoint(x: offset + p(24.0), y: offset + p(24.0))])
|
||||
context.strokePath()
|
||||
|
||||
context.beginPath()
|
||||
context.addLines(between: [CGPoint(x: offset + p(24.0), y: offset + p(12.0)),
|
||||
CGPoint(x: offset + p(12.0), y: offset + p(24.0))])
|
||||
context.strokePath()
|
||||
}
|
||||
}
|
||||
|
||||
@IBDesignable
|
||||
class SafeAreaView: UIView {
|
||||
override func prepareForInterfaceBuilder() {
|
||||
let label = UILabel()
|
||||
label.text = "Safe Area"
|
||||
addSubview(label)
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
label.centerXAnchor.constraint(equalTo: self.centerXAnchor),
|
||||
label.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: -4.0),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBDesignable
|
||||
class OnSafeAreaView: UIView {
|
||||
override func prepareForInterfaceBuilder() {
|
||||
let label = UILabel()
|
||||
label.text = "On Safe Area"
|
||||
addSubview(label)
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
label.centerXAnchor.constraint(equalTo: self.centerXAnchor),
|
||||
label.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: -4.0),
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/10/08.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol LayoutGuideProvider {
|
||||
var topAnchor: NSLayoutYAxisAnchor { get }
|
||||
var bottomAnchor: NSLayoutYAxisAnchor { get }
|
||||
}
|
||||
extension UILayoutGuide: LayoutGuideProvider {}
|
||||
|
||||
class CustomLayoutGuide: LayoutGuideProvider {
|
||||
let topAnchor: NSLayoutYAxisAnchor
|
||||
let bottomAnchor: NSLayoutYAxisAnchor
|
||||
init(topAnchor: NSLayoutYAxisAnchor, bottomAnchor: NSLayoutYAxisAnchor) {
|
||||
self.topAnchor = topAnchor
|
||||
self.bottomAnchor = bottomAnchor
|
||||
}
|
||||
}
|
||||
|
||||
extension UIViewController {
|
||||
var layoutInsets: UIEdgeInsets {
|
||||
if #available(iOS 11.0, *) {
|
||||
return view.safeAreaInsets
|
||||
} else {
|
||||
return UIEdgeInsets(top: topLayoutGuide.length,
|
||||
left: 0.0,
|
||||
bottom: bottomLayoutGuide.length,
|
||||
right: 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
var layoutGuide: LayoutGuideProvider {
|
||||
if #available(iOS 11.0, *) {
|
||||
return view!.safeAreaLayoutGuide
|
||||
} else {
|
||||
return CustomLayoutGuide(topAnchor: topLayoutGuide.bottomAnchor,
|
||||
bottomAnchor: bottomLayoutGuide.topAnchor)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// FloatingModalSampleTests.swift
|
||||
// FloatingModalSampleTests
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/09/18.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import FloatingPanelSample
|
||||
|
||||
class SampleTests: XCTestCase {
|
||||
|
||||
override func setUp() {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func testExample() {
|
||||
// This is an example of a functional test case.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
}
|
||||
|
||||
func testPerformanceExample() {
|
||||
// This is an example of a performance test case.
|
||||
self.measure {
|
||||
// Put the code you want to measure the time of here.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/09/18.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
class SampleUITests: XCTestCase {
|
||||
|
||||
override func setUp() {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
|
||||
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||
continueAfterFailure = false
|
||||
|
||||
// UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
|
||||
XCUIApplication().launch()
|
||||
|
||||
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func testExample() {
|
||||
// Use recording to get started writing UI tests.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,13 +7,13 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
5433F24B21717EA300BDAA5D /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5433F24A21717EA300BDAA5D /* FloatingPanel.framework */; };
|
||||
5433F24C21717EA300BDAA5D /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5433F24A21717EA300BDAA5D /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
548DF95421705BE00041922A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 548DF95321705BE00041922A /* AppDelegate.swift */; };
|
||||
548DF95621705BE00041922A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 548DF95521705BE00041922A /* ViewController.swift */; };
|
||||
548DF95921705BE00041922A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 548DF95721705BE00041922A /* Main.storyboard */; };
|
||||
548DF95B21705BE10041922A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 548DF95A21705BE10041922A /* Assets.xcassets */; };
|
||||
548DF95E21705BE10041922A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 548DF95C21705BE10041922A /* LaunchScreen.storyboard */; };
|
||||
549D23CF233C77CF008EF4D7 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CE233C77CF008EF4D7 /* FloatingPanel.framework */; };
|
||||
549D23D0233C77CF008EF4D7 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CE233C77CF008EF4D7 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
@@ -23,7 +23,7 @@
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
5433F24C21717EA300BDAA5D /* FloatingPanel.framework in Embed Frameworks */,
|
||||
549D23D0233C77CF008EF4D7 /* FloatingPanel.framework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -31,7 +31,6 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
5433F24A21717EA300BDAA5D /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
548DF95021705BE00041922A /* Stocks.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Stocks.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
548DF95321705BE00041922A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
548DF95521705BE00041922A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
@@ -39,6 +38,7 @@
|
||||
548DF95A21705BE10041922A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
548DF95D21705BE10041922A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
548DF95F21705BE10041922A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
549D23CE233C77CF008EF4D7 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -46,7 +46,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5433F24B21717EA300BDAA5D /* FloatingPanel.framework in Frameworks */,
|
||||
549D23CF233C77CF008EF4D7 /* FloatingPanel.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -56,7 +56,7 @@
|
||||
548DF94721705BE00041922A = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5433F24A21717EA300BDAA5D /* FloatingPanel.framework */,
|
||||
549D23CE233C77CF008EF4D7 /* FloatingPanel.framework */,
|
||||
548DF95221705BE00041922A /* Stocks */,
|
||||
548DF95121705BE00041922A /* Products */,
|
||||
);
|
||||
@@ -303,7 +303,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = J3D7L9FHSS;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = Stocks/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -312,7 +312,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.Stocks;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -322,7 +322,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = J3D7L9FHSS;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = Stocks/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -331,7 +331,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.Stocks;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Stocks
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/10/12.
|
||||
// Copyright © 2018 scenee. All rights reserved.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina5_9" orientation="portrait">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
@@ -15,11 +15,11 @@
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="Stocks" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Uop-sw-I6p">
|
||||
<rect key="frame" x="0.0" y="109" width="375" height="624.66666666666663"/>
|
||||
<rect key="frame" x="0.0" y="85" width="375" height="537.5"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" placeholderIntrinsicWidth="375" placeholderIntrinsicHeight="625" image="stocks_list" translatesAutoresizingMaskIntoConstraints="NO" id="XJR-iK-fem">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="625"/>
|
||||
@@ -34,10 +34,10 @@
|
||||
</constraints>
|
||||
</scrollView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="dFl-81-6ok">
|
||||
<rect key="frame" x="0.0" y="733.66666666666663" width="375" height="78.333333333333371"/>
|
||||
<rect key="frame" x="0.0" y="622.5" width="375" height="44.5"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="yahoo_bottom_bar" translatesAutoresizingMaskIntoConstraints="NO" id="NKr-gS-mpx">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44.333333333333336"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44.5"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="44.5" id="B5t-ZF-qUj"/>
|
||||
@@ -52,18 +52,17 @@
|
||||
<constraint firstItem="NKr-gS-mpx" firstAttribute="leading" secondItem="dFl-81-6ok" secondAttribute="leading" id="T2r-kY-JYy"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" axis="vertical" alignment="top" spacing="-8" translatesAutoresizingMaskIntoConstraints="NO" id="f7r-Al-pIN">
|
||||
<rect key="frame" x="16" y="44.000000000000014" width="153.33333333333334" height="56.666666666666664"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="-8" translatesAutoresizingMaskIntoConstraints="NO" id="f7r-Al-pIN">
|
||||
<rect key="frame" x="16" y="20" width="153.5" height="57"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="STOCKS" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="PCG-Wl-fXa">
|
||||
<rect key="frame" x="0.0" y="0.0" width="111.66666666666667" height="32.333333333333336"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="111.5" height="32.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="heavy" pointSize="27"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="OCTOBER 5" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XmK-pu-9g4">
|
||||
<rect key="frame" x="0.0" y="24.333333333333332" width="153.33333333333334" height="32.333333333333343"/>
|
||||
<rect key="frame" x="0.0" y="24.5" width="153.5" height="32.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="heavy" pointSize="27"/>
|
||||
<color key="textColor" red="0.55308091640472412" green="0.55657511949539185" blue="0.57255202531814575" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -75,10 +74,12 @@
|
||||
<constraints>
|
||||
<constraint firstItem="6Tk-OE-BBY" firstAttribute="trailing" secondItem="dFl-81-6ok" secondAttribute="trailing" id="20i-yz-AaQ"/>
|
||||
<constraint firstItem="Uop-sw-I6p" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" id="44w-r8-vYl"/>
|
||||
<constraint firstItem="f7r-Al-pIN" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" constant="16" id="4Bq-Km-eET"/>
|
||||
<constraint firstItem="Uop-sw-I6p" firstAttribute="top" secondItem="6Tk-OE-BBY" secondAttribute="top" constant="65" id="CXL-Dk-8MM"/>
|
||||
<constraint firstItem="Uop-sw-I6p" firstAttribute="trailing" secondItem="6Tk-OE-BBY" secondAttribute="trailing" id="CsO-WF-T8L"/>
|
||||
<constraint firstItem="dFl-81-6ok" firstAttribute="top" secondItem="Uop-sw-I6p" secondAttribute="bottom" id="Cz0-dW-r9H"/>
|
||||
<constraint firstAttribute="bottom" secondItem="dFl-81-6ok" secondAttribute="bottom" id="KGl-8W-5ja"/>
|
||||
<constraint firstItem="f7r-Al-pIN" firstAttribute="top" secondItem="6Tk-OE-BBY" secondAttribute="top" constant="1.4210854715202004e-14" id="Qvt-vQ-PpT"/>
|
||||
<constraint firstItem="dFl-81-6ok" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" id="nlX-Ab-1aI"/>
|
||||
<constraint firstItem="6Tk-OE-BBY" firstAttribute="bottom" secondItem="NKr-gS-mpx" secondAttribute="bottom" id="yeu-NH-Pmp"/>
|
||||
</constraints>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Stocks
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/10/12.
|
||||
// Copyright © 2018 scenee. All rights reserved.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
@@ -34,11 +34,11 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
|
||||
newsVC = storyboard?.instantiateViewController(withIdentifier: "News") as? NewsViewController
|
||||
|
||||
// Add a content view controller
|
||||
fpc.show(newsVC, sender: self)
|
||||
// Set a content view controller
|
||||
fpc.set(contentViewController: newsVC)
|
||||
fpc.track(scrollView: newsVC.scrollView)
|
||||
|
||||
fpc.add(toParent: self, belowView: bottomToolView, animated: false)
|
||||
fpc.addPanel(toParent: self, belowView: bottomToolView, animated: false)
|
||||
|
||||
topBannerView.frame = .zero
|
||||
topBannerView.alpha = 0.0
|
||||
@@ -102,30 +102,25 @@ class NewsViewController: UIViewController {
|
||||
// MARK: My custom layout
|
||||
|
||||
class FloatingPanelStocksLayout: FloatingPanelLayout {
|
||||
public var supportedPositions: [FloatingPanelPosition] {
|
||||
return [.full, .half, .tip]
|
||||
}
|
||||
|
||||
public var initialPosition: FloatingPanelPosition {
|
||||
var initialPosition: FloatingPanelPosition {
|
||||
return .tip
|
||||
}
|
||||
|
||||
public func insetFor(position: FloatingPanelPosition) -> CGFloat? {
|
||||
var topInteractionBuffer: CGFloat { return 0.0 }
|
||||
var bottomInteractionBuffer: CGFloat { return 0.0 }
|
||||
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
|
||||
switch position {
|
||||
case .full: return 56.0
|
||||
case .half: return 262.0
|
||||
case .tip: return 85.0 + 44.0 // Visible + ToolView
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
|
||||
return [
|
||||
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0),
|
||||
surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0),
|
||||
]
|
||||
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
var backdropAlpha: CGFloat = 0.0
|
||||
}
|
||||
|
||||
// MARK: My custom behavior
|
||||
@@ -135,27 +130,16 @@ class FloatingPanelStocksBehavior: FloatingPanelBehavior {
|
||||
return 15.0
|
||||
}
|
||||
|
||||
func interactionAnimator(to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
|
||||
let damping = self.damping(with: velocity)
|
||||
let springTiming = UISpringTimingParameters(dampingRatio: damping,
|
||||
initialVelocity: velocity)
|
||||
let duration = getDuration(with: velocity)
|
||||
return UIViewPropertyAnimator(duration: duration, timingParameters: springTiming)
|
||||
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
|
||||
let timing = timeingCurve(to: targetPosition, with: velocity)
|
||||
return UIViewPropertyAnimator(duration: 0, timingParameters: timing)
|
||||
}
|
||||
|
||||
private func getDuration(with velocity: CGVector) -> TimeInterval {
|
||||
let dy = abs(velocity.dy)
|
||||
switch dy {
|
||||
case ..<1.0:
|
||||
return 0.5
|
||||
case 1.0..<velocityThreshold:
|
||||
let a = ((dy - 1.0) / (velocityThreshold - 1.0))
|
||||
return TimeInterval(0.5 - (0.25 * a))
|
||||
case velocityThreshold...:
|
||||
return 0.25
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
private func timeingCurve(to: FloatingPanelPosition, with velocity: CGVector) -> UITimingCurveProvider {
|
||||
let damping = self.damping(with: velocity)
|
||||
return UISpringTimingParameters(dampingRatio: damping,
|
||||
frequencyResponse: 0.4,
|
||||
initialVelocity: velocity)
|
||||
}
|
||||
|
||||
private func damping(with velocity: CGVector) -> CGFloat {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = "FloatingPanel"
|
||||
s.version = "0.9"
|
||||
s.summary = "FloatingPanel is a simple and easy-to-use UI component of a floating panel interface"
|
||||
s.version = "1.7.2"
|
||||
s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface."
|
||||
s.description = <<-DESC
|
||||
FloatingPanel is a simple and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
|
||||
FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
|
||||
The new interface displays the related contents and utilities in parallel as a user wants.
|
||||
DESC
|
||||
s.homepage = "https://github.com/SCENEE/FloatingPanel"
|
||||
@@ -13,8 +13,7 @@ The new interface displays the related contents and utilities in parallel as a u
|
||||
s.platform = :ios, "10.0"
|
||||
s.source = { :git => "https://github.com/SCENEE/FloatingPanel.git", :tag => "v#{s.version}" }
|
||||
s.source_files = "Framework/Sources/*.swift"
|
||||
s.swift_version = "4.2"
|
||||
s.pod_target_xcconfig = { 'SWIFT_WHOLE_MODULE_OPTIMIZATION' => 'YES', 'APPLICATION_EXTENSION_API_ONLY' => 'YES' }
|
||||
s.swift_versions = ["4.0", "4.2", "5.0"]
|
||||
|
||||
s.framework = "UIKit"
|
||||
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
location = "group:Framework"
|
||||
name = "Framework">
|
||||
<FileRef
|
||||
location = "group:/Users/shin/Workspace/scenee/FloatingPanel/Framework/FloatingPanel.xcodeproj">
|
||||
location = "group:FloatingPanel.xcodeproj">
|
||||
</FileRef>
|
||||
</Group>
|
||||
<Group
|
||||
|
||||
@@ -7,18 +7,27 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
542753C622C49A6E00D17955 /* FloatingPanelLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C522C49A6E00D17955 /* FloatingPanelLayoutTests.swift */; };
|
||||
54352E9621A51A2500CBCA08 /* FloatingPanelTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */; };
|
||||
54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */; };
|
||||
5450EEE421646DF500135936 /* FloatingPanelBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */; };
|
||||
545DB9CB2151169500CA77B8 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 545DB9C12151169500CA77B8 /* FloatingPanel.framework */; };
|
||||
545DB9D02151169500CA77B8 /* ViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9CF2151169500CA77B8 /* ViewTests.swift */; };
|
||||
545DB9D22151169500CA77B8 /* FloatingPanelController.h in Headers */ = {isa = PBXBuildFile; fileRef = 545DB9C42151169500CA77B8 /* FloatingPanelController.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
545DB9D02151169500CA77B8 /* FloatingPanelControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9CF2151169500CA77B8 /* FloatingPanelControllerTests.swift */; };
|
||||
545DB9D22151169500CA77B8 /* FloatingPanel.h in Headers */ = {isa = PBXBuildFile; fileRef = 545DB9C42151169500CA77B8 /* FloatingPanel.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9DD215118C800CA77B8 /* UIExtensions.swift */; };
|
||||
545DB9E021511AC100CA77B8 /* FloatingPanelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */; };
|
||||
545DBA2B2152383100CA77B8 /* GrabberHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */; };
|
||||
546055BF2333C4740069F400 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C722C49A8F00D17955 /* Utils.swift */; };
|
||||
549E944522CF295D0050AECF /* FloatingPanelPositionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549E944422CF295D0050AECF /* FloatingPanelPositionTests.swift */; };
|
||||
54A6B6B122968B530077F348 /* FloatingPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B022968B530077F348 /* FloatingPanelTests.swift */; };
|
||||
54A6B6B622968F710077F348 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54A6B6B522968F710077F348 /* LaunchScreen.storyboard */; };
|
||||
54A6B6B82296A8520077F348 /* FloatingPanelSurfaceViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B72296A8520077F348 /* FloatingPanelSurfaceViewTests.swift */; };
|
||||
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54ABD7AE216CCFF7002E6C13 /* Logger.swift */; };
|
||||
54CDC5D3215B6D5A007D205C /* FloatingPanelSurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */; };
|
||||
54CDC5D5215B6D8D007D205C /* FloatingPanelBackdropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */; };
|
||||
54CFBFC3215CD045006B5735 /* FloatingPanelLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */; };
|
||||
54CFBFC5215CD09C006B5735 /* FloatingPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */; };
|
||||
54CFBFC5215CD09C006B5735 /* FloatingPanelCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC4215CD09C006B5735 /* FloatingPanelCore.swift */; };
|
||||
54E740CD218AFD67005C1A34 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E740CC218AFD67005C1A34 /* AppDelegate.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -29,24 +38,42 @@
|
||||
remoteGlobalIDString = 545DB9C02151169500CA77B8;
|
||||
remoteInfo = FloatingModalController;
|
||||
};
|
||||
54E740DC218AFE9F005C1A34 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 545DB9B82151169500CA77B8 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 54E740C9218AFD67005C1A34;
|
||||
remoteInfo = TestingHost;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
542753C522C49A6E00D17955 /* FloatingPanelLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelLayoutTests.swift; sourceTree = "<group>"; };
|
||||
542753C722C49A8F00D17955 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; };
|
||||
54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelTransitioning.swift; sourceTree = "<group>"; };
|
||||
54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelView.swift; sourceTree = "<group>"; };
|
||||
5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelBehavior.swift; sourceTree = "<group>"; };
|
||||
545DB9C12151169500CA77B8 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
545DB9C42151169500CA77B8 /* FloatingPanelController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FloatingPanelController.h; sourceTree = "<group>"; };
|
||||
545DB9C42151169500CA77B8 /* FloatingPanel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FloatingPanel.h; sourceTree = "<group>"; };
|
||||
545DB9C52151169500CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
545DB9CA2151169500CA77B8 /* FloatingPanelTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FloatingPanelTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
545DB9CF2151169500CA77B8 /* ViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewTests.swift; sourceTree = "<group>"; };
|
||||
545DB9CF2151169500CA77B8 /* FloatingPanelControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelControllerTests.swift; sourceTree = "<group>"; };
|
||||
545DB9D12151169500CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
545DB9DD215118C800CA77B8 /* UIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIExtensions.swift; sourceTree = "<group>"; };
|
||||
545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelController.swift; sourceTree = "<group>"; };
|
||||
545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrabberHandleView.swift; sourceTree = "<group>"; };
|
||||
549E944422CF295D0050AECF /* FloatingPanelPositionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelPositionTests.swift; sourceTree = "<group>"; };
|
||||
54A6B6B022968B530077F348 /* FloatingPanelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelTests.swift; sourceTree = "<group>"; };
|
||||
54A6B6B522968F710077F348 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
54A6B6B72296A8520077F348 /* FloatingPanelSurfaceViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelSurfaceViewTests.swift; sourceTree = "<group>"; };
|
||||
54ABD7AE216CCFF7002E6C13 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
||||
54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelSurfaceView.swift; sourceTree = "<group>"; };
|
||||
54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelBackdropView.swift; sourceTree = "<group>"; };
|
||||
54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelLayout.swift; sourceTree = "<group>"; };
|
||||
54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanel.swift; sourceTree = "<group>"; };
|
||||
54CFBFC4215CD09C006B5735 /* FloatingPanelCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelCore.swift; sourceTree = "<group>"; };
|
||||
54E740CA218AFD67005C1A34 /* FloatingPanelTesting.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FloatingPanelTesting.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
54E740CC218AFD67005C1A34 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
54E740D8218AFD6A005C1A34 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -65,6 +92,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
54E740C7218AFD67005C1A34 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
@@ -73,6 +107,7 @@
|
||||
children = (
|
||||
545DB9C32151169500CA77B8 /* Sources */,
|
||||
545DB9CE2151169500CA77B8 /* Tests */,
|
||||
54E740CB218AFD67005C1A34 /* TestingApp */,
|
||||
545DB9C22151169500CA77B8 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
@@ -82,6 +117,7 @@
|
||||
children = (
|
||||
545DB9C12151169500CA77B8 /* FloatingPanel.framework */,
|
||||
545DB9CA2151169500CA77B8 /* FloatingPanelTests.xctest */,
|
||||
54E740CA218AFD67005C1A34 /* FloatingPanelTesting.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -90,11 +126,13 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
545DB9C52151169500CA77B8 /* Info.plist */,
|
||||
545DB9C42151169500CA77B8 /* FloatingPanelController.h */,
|
||||
545DB9C42151169500CA77B8 /* FloatingPanel.h */,
|
||||
545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */,
|
||||
54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */,
|
||||
54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */,
|
||||
54CFBFC4215CD09C006B5735 /* FloatingPanelCore.swift */,
|
||||
54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */,
|
||||
5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */,
|
||||
54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */,
|
||||
54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */,
|
||||
54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */,
|
||||
545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */,
|
||||
@@ -107,12 +145,27 @@
|
||||
545DB9CE2151169500CA77B8 /* Tests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
545DB9CF2151169500CA77B8 /* ViewTests.swift */,
|
||||
54A6B6B022968B530077F348 /* FloatingPanelTests.swift */,
|
||||
545DB9CF2151169500CA77B8 /* FloatingPanelControllerTests.swift */,
|
||||
542753C522C49A6E00D17955 /* FloatingPanelLayoutTests.swift */,
|
||||
54A6B6B72296A8520077F348 /* FloatingPanelSurfaceViewTests.swift */,
|
||||
549E944422CF295D0050AECF /* FloatingPanelPositionTests.swift */,
|
||||
542753C722C49A8F00D17955 /* Utils.swift */,
|
||||
545DB9D12151169500CA77B8 /* Info.plist */,
|
||||
);
|
||||
path = Tests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54E740CB218AFD67005C1A34 /* TestingApp */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54E740CC218AFD67005C1A34 /* AppDelegate.swift */,
|
||||
54E740D8218AFD6A005C1A34 /* Info.plist */,
|
||||
54A6B6B522968F710077F348 /* LaunchScreen.storyboard */,
|
||||
);
|
||||
path = TestingApp;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
@@ -120,7 +173,7 @@
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
545DB9D22151169500CA77B8 /* FloatingPanelController.h in Headers */,
|
||||
545DB9D22151169500CA77B8 /* FloatingPanel.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -157,19 +210,37 @@
|
||||
);
|
||||
dependencies = (
|
||||
545DB9CD2151169500CA77B8 /* PBXTargetDependency */,
|
||||
54E740DD218AFE9F005C1A34 /* PBXTargetDependency */,
|
||||
);
|
||||
name = FloatingPanelTests;
|
||||
productName = FloatingModalControllerTests;
|
||||
productReference = 545DB9CA2151169500CA77B8 /* FloatingPanelTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
54E740C9218AFD67005C1A34 /* TestingApp */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 54E740D9218AFD6A005C1A34 /* Build configuration list for PBXNativeTarget "TestingApp" */;
|
||||
buildPhases = (
|
||||
54E740C6218AFD67005C1A34 /* Sources */,
|
||||
54E740C7218AFD67005C1A34 /* Frameworks */,
|
||||
54E740C8218AFD67005C1A34 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = TestingApp;
|
||||
productName = TestingHost;
|
||||
productReference = 54E740CA218AFD67005C1A34 /* FloatingPanelTesting.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
545DB9B82151169500CA77B8 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1000;
|
||||
LastSwiftUpdateCheck = 1010;
|
||||
LastUpgradeCheck = 1000;
|
||||
ORGANIZATIONNAME = scenee;
|
||||
TargetAttributes = {
|
||||
@@ -179,6 +250,10 @@
|
||||
};
|
||||
545DB9C92151169500CA77B8 = {
|
||||
CreatedOnToolsVersion = 10.0;
|
||||
TestTargetID = 54E740C9218AFD67005C1A34;
|
||||
};
|
||||
54E740C9218AFD67005C1A34 = {
|
||||
CreatedOnToolsVersion = 10.1;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -188,6 +263,7 @@
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 545DB9B72151169500CA77B8;
|
||||
productRefGroup = 545DB9C22151169500CA77B8 /* Products */;
|
||||
@@ -196,6 +272,7 @@
|
||||
targets = (
|
||||
545DB9C02151169500CA77B8 /* FloatingPanel */,
|
||||
545DB9C92151169500CA77B8 /* FloatingPanelTests */,
|
||||
54E740C9218AFD67005C1A34 /* TestingApp */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -215,6 +292,14 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
54E740C8218AFD67005C1A34 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
54A6B6B622968F710077F348 /* LaunchScreen.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -225,11 +310,13 @@
|
||||
54CDC5D3215B6D5A007D205C /* FloatingPanelSurfaceView.swift in Sources */,
|
||||
54CFBFC3215CD045006B5735 /* FloatingPanelLayout.swift in Sources */,
|
||||
54CDC5D5215B6D8D007D205C /* FloatingPanelBackdropView.swift in Sources */,
|
||||
54CFBFC5215CD09C006B5735 /* FloatingPanel.swift in Sources */,
|
||||
54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */,
|
||||
54CFBFC5215CD09C006B5735 /* FloatingPanelCore.swift in Sources */,
|
||||
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */,
|
||||
545DB9E021511AC100CA77B8 /* FloatingPanelController.swift in Sources */,
|
||||
5450EEE421646DF500135936 /* FloatingPanelBehavior.swift in Sources */,
|
||||
545DBA2B2152383100CA77B8 /* GrabberHandleView.swift in Sources */,
|
||||
54352E9621A51A2500CBCA08 /* FloatingPanelTransitioning.swift in Sources */,
|
||||
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -238,7 +325,20 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
545DB9D02151169500CA77B8 /* ViewTests.swift in Sources */,
|
||||
54A6B6B122968B530077F348 /* FloatingPanelTests.swift in Sources */,
|
||||
545DB9D02151169500CA77B8 /* FloatingPanelControllerTests.swift in Sources */,
|
||||
549E944522CF295D0050AECF /* FloatingPanelPositionTests.swift in Sources */,
|
||||
542753C622C49A6E00D17955 /* FloatingPanelLayoutTests.swift in Sources */,
|
||||
54A6B6B82296A8520077F348 /* FloatingPanelSurfaceViewTests.swift in Sources */,
|
||||
546055BF2333C4740069F400 /* Utils.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
54E740C6218AFD67005C1A34 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
54E740CD218AFD67005C1A34 /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -250,6 +350,11 @@
|
||||
target = 545DB9C02151169500CA77B8 /* FloatingPanel */;
|
||||
targetProxy = 545DB9CC2151169500CA77B8 /* PBXContainerItemProxy */;
|
||||
};
|
||||
54E740DD218AFE9F005C1A34 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 54E740C9218AFD67005C1A34 /* TestingApp */;
|
||||
targetProxy = 54E740DC218AFE9F005C1A34 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
@@ -378,6 +483,8 @@
|
||||
545DB9D62151169500CA77B8 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -397,8 +504,9 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 4.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -406,6 +514,8 @@
|
||||
545DB9D72151169500CA77B8 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -424,7 +534,8 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanel;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_VERSION = 4.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
@@ -434,7 +545,9 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = Tests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -442,8 +555,9 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalControllerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FloatingPanelTesting.app/FloatingPanelTesting";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -452,7 +566,9 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = Tests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -460,11 +576,177 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalControllerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FloatingPanelTesting.app/FloatingPanelTesting";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
54E740DA218AFD6A005C1A34 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = TestingApp/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTesting;
|
||||
PRODUCT_NAME = FloatingPanelTesting;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
54E740DB218AFD6A005C1A34 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = TestingApp/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTesting;
|
||||
PRODUCT_NAME = FloatingPanelTesting;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
54E79ADF224F6C9800717BC6 /* Test */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
name = Test;
|
||||
};
|
||||
54E79AE0224F6C9800717BC6 /* Test */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = Sources/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanel;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "TEST DEBUG __FP_LOG";
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 4.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Test;
|
||||
};
|
||||
54E79AE1224F6C9800717BC6 /* Test */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = Tests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalControllerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FloatingPanelTesting.app/FloatingPanelTesting";
|
||||
};
|
||||
name = Test;
|
||||
};
|
||||
54E8AC6A2286CFB6000C5A12 /* Test */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = TestingApp/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTesting;
|
||||
PRODUCT_NAME = FloatingPanelTesting;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Test;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
@@ -472,6 +754,7 @@
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
545DB9D32151169500CA77B8 /* Debug */,
|
||||
54E79ADF224F6C9800717BC6 /* Test */,
|
||||
545DB9D42151169500CA77B8 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
@@ -481,6 +764,7 @@
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
545DB9D62151169500CA77B8 /* Debug */,
|
||||
54E79AE0224F6C9800717BC6 /* Test */,
|
||||
545DB9D72151169500CA77B8 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
@@ -490,11 +774,22 @@
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
545DB9D92151169500CA77B8 /* Debug */,
|
||||
54E79AE1224F6C9800717BC6 /* Test */,
|
||||
545DB9DA2151169500CA77B8 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
54E740D9218AFD6A005C1A34 /* Build configuration list for PBXNativeTarget "TestingApp" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
54E740DA218AFD6A005C1A34 /* Debug */,
|
||||
54E740DB218AFD6A005C1A34 /* Release */,
|
||||
54E8AC6A2286CFB6000C5A12 /* Test */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 545DB9B82151169500CA77B8 /* Project object */;
|
||||
|
||||
@@ -23,12 +23,33 @@
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
buildConfiguration = "Test"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES"
|
||||
testExecutionOrdering = "random">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "545DB9C92151169500CA77B8"
|
||||
BuildableName = "FloatingPanelTests.xctest"
|
||||
BlueprintName = "FloatingPanelTests"
|
||||
ReferencedContainer = "container:FloatingPanel.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "545DB9C02151169500CA77B8"
|
||||
BuildableName = "FloatingPanel.framework"
|
||||
BlueprintName = "FloatingPanel"
|
||||
ReferencedContainer = "container:FloatingPanel.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/09/18.
|
||||
// Copyright © 2018 scenee. All rights reserved.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
@@ -1,415 +0,0 @@
|
||||
//
|
||||
// Copyright © 2018 scenee. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
///
|
||||
/// FloatingPanel presentation model
|
||||
///
|
||||
class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
/* Cause 'terminating with uncaught exception of type NSException' error on Swift Playground
|
||||
unowned let view: UIView
|
||||
*/
|
||||
let surfaceView: FloatingPanelSurfaceView
|
||||
let backdropView: FloatingPanelBackdropView
|
||||
|
||||
private unowned let viewcontroller: FloatingPanelController
|
||||
|
||||
weak var scrollView: UIScrollView? {
|
||||
didSet {
|
||||
configureScrollable()
|
||||
}
|
||||
}
|
||||
|
||||
var safeAreaInsets: UIEdgeInsets! {
|
||||
get {
|
||||
return layoutAdapter.safeAreaInsets
|
||||
}
|
||||
set {
|
||||
layoutAdapter.safeAreaInsets = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var state: FloatingPanelPosition = .tip {
|
||||
didSet {
|
||||
switch state {
|
||||
case .full:
|
||||
backdropView.alpha = layoutAdapter.layout.backdropAlpha
|
||||
default:
|
||||
backdropView.alpha = 0.0
|
||||
}
|
||||
configureScrollable()
|
||||
}
|
||||
}
|
||||
|
||||
var layoutAdapter: FloatingPanelLayoutAdapter
|
||||
var behavior: FloatingPanelBehavior
|
||||
private var animator: UIViewPropertyAnimator?
|
||||
private let panGesture: UIPanGestureRecognizer
|
||||
private var initialFrame: CGRect = .zero
|
||||
private var transOffsetY: CGFloat = 0
|
||||
private var interactionInProgress: Bool = false
|
||||
|
||||
init(_ vc: FloatingPanelController, layout: FloatingPanelLayout, behavior: FloatingPanelBehavior) {
|
||||
viewcontroller = vc
|
||||
surfaceView = vc.view as! FloatingPanelSurfaceView
|
||||
backdropView = FloatingPanelBackdropView()
|
||||
backdropView.backgroundColor = .black
|
||||
backdropView.alpha = 0.0
|
||||
|
||||
self.layoutAdapter = FloatingPanelLayoutAdapter(surfaceView: surfaceView, layout: layout)
|
||||
self.behavior = behavior
|
||||
|
||||
panGesture = UIPanGestureRecognizer()
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
panGesture.name = "FloatingPanelSurface"
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
surfaceView.addGestureRecognizer(panGesture)
|
||||
panGesture.addTarget(self, action: #selector(handle(panGesture:)))
|
||||
panGesture.delegate = self
|
||||
}
|
||||
|
||||
func layoutViews(in vc: UIViewController) {
|
||||
unowned let view = vc.view!
|
||||
|
||||
view.insertSubview(backdropView, belowSubview: surfaceView)
|
||||
backdropView.frame = view.bounds
|
||||
|
||||
layoutAdapter.prepareLayout(toParent: vc)
|
||||
}
|
||||
|
||||
func move(to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
|
||||
if animated {
|
||||
let animator = behavior.presentAnimator(from: state, to: to)
|
||||
animator.addAnimations { [weak self] in
|
||||
self?.updateLayout(to: to)
|
||||
self?.state = to
|
||||
}
|
||||
animator.addCompletion { _ in
|
||||
completion?()
|
||||
}
|
||||
animator.startAnimation()
|
||||
} else {
|
||||
self.updateLayout(to: to)
|
||||
self.state = to
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
func present(animated: Bool, completion: (() -> Void)? = nil) {
|
||||
self.layoutAdapter.activateLayout(of: nil)
|
||||
move(to: layoutAdapter.layout.initialPosition, animated: animated, completion: completion)
|
||||
}
|
||||
|
||||
func dismiss(animated: Bool, completion: (() -> Void)? = nil) {
|
||||
if animated {
|
||||
let animator = behavior.dismissAnimator(from: state)
|
||||
animator.addAnimations { [weak self] in
|
||||
self?.updateLayout(to: nil)
|
||||
}
|
||||
animator.addCompletion { _ in
|
||||
completion?()
|
||||
}
|
||||
animator.startAnimation()
|
||||
} else {
|
||||
self.updateLayout(to: nil)
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
|
||||
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
log.debug("gestureRecognizer", gestureRecognizer,
|
||||
"shouldRecognizeSimultaneouslyWith", otherGestureRecognizer)
|
||||
if #available(iOS 11.0, *) {
|
||||
log.debug("gestureRecognizer",
|
||||
String(describing: gestureRecognizer.name),
|
||||
"shouldRecognizeSimultaneouslyWith",
|
||||
String(describing: otherGestureRecognizer.name))
|
||||
}
|
||||
|
||||
switch (gestureRecognizer, otherGestureRecognizer) {
|
||||
case (panGesture, scrollView?.panGestureRecognizer):
|
||||
return state == .full
|
||||
case (panGesture, is UIPanGestureRecognizer):
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
// Do not begin any gestures until the pan gesture fails at non-full position.
|
||||
return gestureRecognizer == panGesture && state != .full
|
||||
}
|
||||
|
||||
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
private func configureScrollable() {
|
||||
switch state {
|
||||
case .full:
|
||||
scrollView?.isScrollEnabled = true
|
||||
default:
|
||||
scrollView?.isScrollEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handle(panGesture: UIPanGestureRecognizer) {
|
||||
let translation = panGesture.translation(in: panGesture.view!.superview)
|
||||
let velocity = panGesture.velocity(in: panGesture.view)
|
||||
let location = panGesture.location(in: panGesture.view)
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
log.debug("Gesture >>>>", panGesture.name!)
|
||||
}
|
||||
if let scrollView = scrollView, scrollView.frame.contains(location), interactionInProgress == false {
|
||||
log.debug("ScrollView.contentOffset >>>", scrollView.contentOffset)
|
||||
if state == .full {
|
||||
if scrollView.contentOffset.y > scrollView.contentOffsetZero.y {
|
||||
return
|
||||
}
|
||||
if scrollView.isDecelerating {
|
||||
return
|
||||
}
|
||||
if interactionInProgress == false, velocity.y < 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
scrollView.contentOffset.y = scrollView.contentOffsetZero.y
|
||||
}
|
||||
log.debug(panGesture.state, ">>>", "{ translation: \(translation), velocity: \(velocity) }")
|
||||
switch panGesture.state {
|
||||
case .began:
|
||||
panningBegan()
|
||||
case .changed:
|
||||
panningChange(with: translation)
|
||||
case .ended, .cancelled, .failed:
|
||||
panningEnd(with: translation, velocity: velocity)
|
||||
case .possible:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func panningBegan() {
|
||||
// A user interaction does not always start from Began state of the pan gesture
|
||||
// because it can be recognized in scrolling a content in a content view controller.
|
||||
// So I don't nothing here.
|
||||
log.debug("panningBegan \(initialFrame)")
|
||||
}
|
||||
|
||||
private func panningChange(with translation: CGPoint) {
|
||||
log.debug("panningChange")
|
||||
if interactionInProgress == false {
|
||||
startInteraction(with: translation)
|
||||
}
|
||||
var frame = initialFrame
|
||||
frame.origin.y = getCurrentY(from: initialFrame, with: translation)
|
||||
surfaceView.frame = frame
|
||||
viewcontroller.delegate?.floatingPanelDidMove(viewcontroller)
|
||||
backdropView.alpha = updateBackdropAlpha(with: translation)
|
||||
}
|
||||
|
||||
private func panningEnd(with translation: CGPoint, velocity: CGPoint) {
|
||||
log.debug("panningEnd")
|
||||
if interactionInProgress == false {
|
||||
initialFrame = surfaceView.frame
|
||||
}
|
||||
|
||||
let targetPosition = self.targetPosition(with: translation, velocity: velocity)
|
||||
let distance = self.distance(to: targetPosition, with: translation)
|
||||
|
||||
endInteraction(for: targetPosition)
|
||||
viewcontroller.delegate?.floatingPanelDidEndDragging(viewcontroller, withVelocity: velocity, targetPosition: targetPosition)
|
||||
viewcontroller.delegate?.floatingPanelWillBeginDecelerating(viewcontroller)
|
||||
startAnimation(to: targetPosition, at: distance, with: velocity)
|
||||
}
|
||||
|
||||
private func startInteraction(with translation: CGPoint) {
|
||||
log.debug("startInteraction")
|
||||
initialFrame = surfaceView.frame
|
||||
transOffsetY = translation.y
|
||||
viewcontroller.delegate?.floatingPanelWillBeginDragging(viewcontroller)
|
||||
if let scrollView = scrollView {
|
||||
scrollView.isScrollEnabled = false
|
||||
}
|
||||
|
||||
interactionInProgress = true
|
||||
}
|
||||
|
||||
private func endInteraction(for targetPosition: FloatingPanelPosition) {
|
||||
log.debug("endInteraction for \(targetPosition)")
|
||||
if let scrollView = scrollView {
|
||||
if targetPosition == .full {
|
||||
scrollView.isScrollEnabled = true
|
||||
}
|
||||
}
|
||||
interactionInProgress = false
|
||||
}
|
||||
|
||||
private func getCurrentY(from rect: CGRect, with translation: CGPoint) -> CGFloat {
|
||||
let dy = translation.y - transOffsetY
|
||||
let y = rect.offsetBy(dx: 0.0, dy: dy).origin.y
|
||||
|
||||
let topY = layoutAdapter.topY
|
||||
let topInset = layoutAdapter.topInset
|
||||
let topBuffer = layoutAdapter.layout.topInteractionBuffer
|
||||
|
||||
let bottomY = layoutAdapter.bottomY
|
||||
let bottomBuffer = layoutAdapter.layout.bottomInteractionBuffer
|
||||
|
||||
return max(topY - topInset + topBuffer, min(bottomY + bottomBuffer, y))
|
||||
}
|
||||
|
||||
private func startAnimation(to targetPosition: FloatingPanelPosition, at distance: CGFloat, with velocity: CGPoint) {
|
||||
let targetY = layoutAdapter.positionY(for: targetPosition)
|
||||
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: velocity.y/distance) : .zero
|
||||
let animator = behavior.interactionAnimator(to: targetPosition, with: velocityVector)
|
||||
animator.addAnimations { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if self.state == targetPosition {
|
||||
self.surfaceView.frame.origin.y = targetY
|
||||
} else {
|
||||
self.updateLayout(to: targetPosition)
|
||||
}
|
||||
self.state = targetPosition
|
||||
}
|
||||
animator.addCompletion { [weak self] pos in
|
||||
guard let self = self else { return }
|
||||
guard
|
||||
self.interactionInProgress == false,
|
||||
animator == self.animator,
|
||||
pos == .end
|
||||
else { return }
|
||||
self.finishAnimation(at: targetPosition)
|
||||
}
|
||||
animator.startAnimation()
|
||||
self.animator = animator
|
||||
}
|
||||
|
||||
private func finishAnimation(at targetPosition: FloatingPanelPosition) {
|
||||
log.debug("finishAnimation \(targetPosition)")
|
||||
self.animator = nil
|
||||
self.viewcontroller.delegate?.floatingPanelDidEndDecelerating(self.viewcontroller)
|
||||
}
|
||||
|
||||
private func updateLayout(to target: FloatingPanelPosition?) {
|
||||
self.layoutAdapter.activateLayout(of: target)
|
||||
}
|
||||
|
||||
private func updateBackdropAlpha(with translation: CGPoint) -> CGFloat {
|
||||
let topY = layoutAdapter.topY
|
||||
let middleY = layoutAdapter.middleY
|
||||
let currentY = getCurrentY(from: initialFrame, with: translation)
|
||||
return (1 - (currentY - topY) / (middleY - topY)) * layoutAdapter.layout.backdropAlpha
|
||||
}
|
||||
|
||||
// Animation handling
|
||||
private func distance(to targetPosition: FloatingPanelPosition, with translation: CGPoint) -> CGFloat {
|
||||
let topY = layoutAdapter.topY
|
||||
let middleY = layoutAdapter.middleY
|
||||
let bottomY = layoutAdapter.bottomY
|
||||
let currentY = getCurrentY(from: initialFrame, with: translation)
|
||||
switch targetPosition {
|
||||
case .full:
|
||||
return CGFloat(fabs(Double(currentY - topY)))
|
||||
case .half:
|
||||
return CGFloat(fabs(Double(currentY - middleY)))
|
||||
case .tip:
|
||||
return CGFloat(fabs(Double(currentY - bottomY)))
|
||||
}
|
||||
}
|
||||
|
||||
// Distance travelled after decelerating to zero velocity at a constant rate.
|
||||
// Refer to the slides p176 of [Designing Fluid Interfaces](https://developer.apple.com/videos/play/wwdc2018/803/)
|
||||
private func project(initialVelocity: CGFloat) -> CGFloat {
|
||||
let decelerationRate = UIScrollView.DecelerationRate.normal.rawValue
|
||||
return (initialVelocity / 1000.0) * decelerationRate / (1.0 - decelerationRate)
|
||||
}
|
||||
|
||||
private func targetPosition(with translation: CGPoint, velocity: CGPoint) -> (FloatingPanelPosition) {
|
||||
let currentY = getCurrentY(from: initialFrame, with: translation)
|
||||
let supportedPositions = Set(layoutAdapter.layout.supportedPositions)
|
||||
|
||||
assert(supportedPositions.count > 1)
|
||||
|
||||
switch supportedPositions {
|
||||
case Set([.full, .half]):
|
||||
return targetPosition(from: [.half, .tip], at: currentY, velocity: velocity)
|
||||
case Set([.half, .tip]):
|
||||
return targetPosition(from: [.half, .tip], at: currentY, velocity: velocity)
|
||||
case Set([.full, .tip]):
|
||||
return targetPosition(from: [.full, .tip], at: currentY, velocity: velocity)
|
||||
default:
|
||||
/*
|
||||
[topY|full]---[th1]---[middleY|default]---[th2]---[bottomY|collapsed]
|
||||
*/
|
||||
let topY = layoutAdapter.topY
|
||||
let middleY = layoutAdapter.middleY
|
||||
let bottomY = layoutAdapter.bottomY
|
||||
|
||||
let th1 = (topY + middleY) / 2
|
||||
let th2 = (middleY + bottomY) / 2
|
||||
|
||||
switch currentY {
|
||||
case ..<th1:
|
||||
if project(initialVelocity: velocity.y) >= (middleY - currentY) {
|
||||
return .half
|
||||
} else {
|
||||
return .full
|
||||
}
|
||||
case ...middleY:
|
||||
if project(initialVelocity: velocity.y) <= (topY - currentY) {
|
||||
return .full
|
||||
} else {
|
||||
return .half
|
||||
}
|
||||
case ..<th2:
|
||||
if project(initialVelocity: velocity.y) >= (bottomY - currentY) {
|
||||
return .tip
|
||||
} else {
|
||||
return .half
|
||||
}
|
||||
default:
|
||||
if project(initialVelocity: velocity.y) <= (middleY - currentY) {
|
||||
return .half
|
||||
} else {
|
||||
return .tip
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func targetPosition(from positions: [FloatingPanelPosition], at currentY: CGFloat, velocity: CGPoint) -> FloatingPanelPosition {
|
||||
assert(positions.count == 2)
|
||||
|
||||
let top = positions[0]
|
||||
let bottom = positions[1]
|
||||
|
||||
let topY = layoutAdapter.positionY(for: top)
|
||||
let bottomY = layoutAdapter.positionY(for: bottom)
|
||||
|
||||
let th = (topY + bottomY) / 2
|
||||
|
||||
switch currentY {
|
||||
case ..<th:
|
||||
if project(initialVelocity: velocity.y) >= (bottomY - currentY) {
|
||||
return bottom
|
||||
} else {
|
||||
return top
|
||||
}
|
||||
default:
|
||||
if project(initialVelocity: velocity.y) <= (topY - currentY) {
|
||||
return top
|
||||
} else {
|
||||
return bottom
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/09/26.
|
||||
// Copyright © 2018 scenee. All rights reserved.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// A view that presents a backdrop interface behind a floating panel.
|
||||
public class FloatingPanelBackdropView: UIView { }
|
||||
public class FloatingPanelBackdropView: UIView {
|
||||
public var dismissalTapGestureRecognizer: UITapGestureRecognizer!
|
||||
}
|
||||
|
||||
@@ -1,61 +1,150 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/10/03.
|
||||
// Copyright © 2018 scenee. All rights reserved.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
public protocol FloatingPanelBehavior {
|
||||
// Returns a UIViewPropertyAnimator object in interacting a floating panel by a user pan gesture
|
||||
func interactionAnimator(to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator
|
||||
/// Asks the behavior if the floating panel should project a momentum of a user interaction to move the proposed position.
|
||||
///
|
||||
/// The default implementation of this method returns true. This method is called for a layout to support all positions(tip, half and full).
|
||||
/// Therefore, `proposedTargetPosition` can only be `FloatingPanelPosition.tip` or `FloatingPanelPosition.full`.
|
||||
func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool
|
||||
|
||||
// Returns a UIViewPropertyAnimator object to present a floating panel
|
||||
func presentAnimator(from: FloatingPanelPosition, to: FloatingPanelPosition) -> UIViewPropertyAnimator
|
||||
// Returns a UIViewPropertyAnimator object to dismiss a floating panel
|
||||
func dismissAnimator(from: FloatingPanelPosition) -> UIViewPropertyAnimator
|
||||
/// Returns a deceleration rate to calculate a target position projected a dragging momentum.
|
||||
///
|
||||
/// The default implementation of this method returns the normal deceleration rate of UIScrollView.
|
||||
func momentumProjectionRate(_ fpc: FloatingPanelController) -> CGFloat
|
||||
|
||||
/// Returns the progress to redirect to the previous position.
|
||||
///
|
||||
/// The progress is represented by a floating-point value between 0.0 and 1.0, inclusive, where 1.0 indicates the floating panel is impossible to move to the next position. The default value is 0.5. Values less than 0.0 and greater than 1.0 are pinned to those limits.
|
||||
func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> CGFloat
|
||||
|
||||
/// Returns a UIViewPropertyAnimator object to project a floating panel to a position on finger up if the user dragged.
|
||||
///
|
||||
/// - Attention:
|
||||
/// By default, it returns a non-interruptible animator to prevent a propagation of the animation to a content view.
|
||||
/// However returning an interruptible animator is working well depending on a content view and it can be better
|
||||
/// than using a non-interruptible one.
|
||||
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator
|
||||
|
||||
/// Returns a UIViewPropertyAnimator object to add a floating panel to a position.
|
||||
///
|
||||
/// Its animator instance will be used to animate the surface view in `FloatingPanelController.addPanel(toParent:belowView:animated:)`.
|
||||
/// Default is an animator with ease-in-out curve and 0.25 sec duration.
|
||||
func addAnimator(_ fpc: FloatingPanelController, to: FloatingPanelPosition) -> UIViewPropertyAnimator
|
||||
|
||||
/// Returns a UIViewPropertyAnimator object to remove a floating panel from a position.
|
||||
///
|
||||
/// Its animator instance will be used to animate the surface view in `FloatingPanelController.removePanelFromParent(animated:completion:)`.
|
||||
/// Default is an animator with ease-in-out curve and 0.25 sec duration.
|
||||
func removeAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition) -> UIViewPropertyAnimator
|
||||
|
||||
/// Returns a UIViewPropertyAnimator object to move a floating panel from a position to a position.
|
||||
///
|
||||
/// Its animator instance will be used to animate the surface view in `FloatingPanelController.move(to:animated:completion:)`.
|
||||
/// Default is an animator with ease-in-out curve and 0.25 sec duration.
|
||||
func moveAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> UIViewPropertyAnimator
|
||||
|
||||
/// Returns a y-axis velocity to invoke a removal interaction at the bottom position.
|
||||
///
|
||||
/// Default is 10.0. This method is called when FloatingPanelController.isRemovalInteractionEnabled is true.
|
||||
var removalVelocity: CGFloat { get }
|
||||
|
||||
/// Returns the threshold of the transition to invoke a removal interaction at the bottom position.
|
||||
///
|
||||
/// The progress is represented by a floating-point value between 0.0 and 1.0, inclusive, where 1.0 indicates the floating panel is impossible to invoke the removal interaction. The default value is 0.5. Values less than 0.0 and greater than 1.0 are pinned to those limits. This method is called when FloatingPanelController.isRemovalInteractionEnabled is true.
|
||||
var removalProgress: CGFloat { get }
|
||||
|
||||
/// Returns a UIViewPropertyAnimator object to remove a floating panel with a velocity interactively at the bottom position.
|
||||
///
|
||||
/// Default is a spring animator with 1.0 damping ratio. This method is called when FloatingPanelController.isRemovalInteractionEnabled is true.
|
||||
func removalInteractionAnimator(_ fpc: FloatingPanelController, with velocity: CGVector) -> UIViewPropertyAnimator
|
||||
|
||||
|
||||
/// Asks the behavior whether the rubber band effect is enabled in moving over a given edge of the surface view.
|
||||
///
|
||||
/// This method allows the behavior to activate the rubber band effect to a given edge of the surface view. By default, the effect is disabled.
|
||||
func allowsRubberBanding(for edge: UIRectEdge) -> Bool
|
||||
}
|
||||
|
||||
public extension FloatingPanelBehavior {
|
||||
func presentAnimator(from: FloatingPanelPosition, to: FloatingPanelPosition) -> UIViewPropertyAnimator {
|
||||
func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func momentumProjectionRate(_ fpc: FloatingPanelController) -> CGFloat {
|
||||
#if swift(>=4.2)
|
||||
return UIScrollView.DecelerationRate.normal.rawValue
|
||||
#else
|
||||
return UIScrollViewDecelerationRateNormal
|
||||
#endif
|
||||
}
|
||||
|
||||
func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> CGFloat {
|
||||
return 0.5
|
||||
}
|
||||
|
||||
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
|
||||
return defaultBehavior.interactionAnimator(fpc, to: targetPosition, with: velocity)
|
||||
}
|
||||
|
||||
func addAnimator(_ fpc: FloatingPanelController, to: FloatingPanelPosition) -> UIViewPropertyAnimator {
|
||||
return UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut)
|
||||
}
|
||||
|
||||
func dismissAnimator(from: FloatingPanelPosition) -> UIViewPropertyAnimator {
|
||||
func removeAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition) -> UIViewPropertyAnimator {
|
||||
return UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut)
|
||||
}
|
||||
|
||||
func moveAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> UIViewPropertyAnimator {
|
||||
return UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut)
|
||||
}
|
||||
|
||||
var removalVelocity: CGFloat {
|
||||
return 10.0
|
||||
}
|
||||
|
||||
var removalProgress: CGFloat {
|
||||
return 0.5
|
||||
}
|
||||
|
||||
func removalInteractionAnimator(_ fpc: FloatingPanelController, with velocity: CGVector) -> UIViewPropertyAnimator {
|
||||
log.debug("velocity", velocity)
|
||||
let timing = UISpringTimingParameters(dampingRatio: 1.0,
|
||||
frequencyResponse: 0.3,
|
||||
initialVelocity: velocity)
|
||||
return UIViewPropertyAnimator(duration: 0, timingParameters: timing)
|
||||
}
|
||||
|
||||
func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
|
||||
func interactionAnimator(to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
|
||||
let timing = timeingCurve(to: targetPosition, with: velocity)
|
||||
let duration = getDuration(with: velocity)
|
||||
return UIViewPropertyAnimator(duration: duration, timingParameters: timing)
|
||||
private let defaultBehavior = FloatingPanelDefaultBehavior()
|
||||
|
||||
public class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
|
||||
public init() { }
|
||||
|
||||
public func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
|
||||
let timing = timeingCurve(with: velocity)
|
||||
let animator = UIViewPropertyAnimator(duration: 0, timingParameters: timing)
|
||||
animator.isInterruptible = false // Prevent a propagation of the animation(spring etc) to a content view
|
||||
return animator
|
||||
}
|
||||
|
||||
private func timeingCurve(with velocity: CGVector) -> UITimingCurveProvider {
|
||||
log.debug("velocity", velocity)
|
||||
let damping = self.getDamping(with: velocity)
|
||||
return UISpringTimingParameters(dampingRatio: damping,
|
||||
frequencyResponse: 0.3,
|
||||
initialVelocity: velocity)
|
||||
}
|
||||
|
||||
private let velocityThreshold: CGFloat = 8.0
|
||||
private func getDuration(with velocity: CGVector) -> TimeInterval {
|
||||
let dy = abs(velocity.dy)
|
||||
switch dy {
|
||||
case ..<1.0:
|
||||
return 0.6
|
||||
case 1.0..<velocityThreshold:
|
||||
let a = ((dy - 1.0) / (velocityThreshold - 1.0))
|
||||
return TimeInterval(0.6 - (0.2 * a))
|
||||
case velocityThreshold...:
|
||||
return 0.4
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
private func timeingCurve(to: FloatingPanelPosition, with velocity: CGVector) -> UITimingCurveProvider {
|
||||
log.debug("velocity", velocity)
|
||||
let damping = self.getDamping(with: velocity)
|
||||
let springTiming = UISpringTimingParameters(dampingRatio: damping,
|
||||
initialVelocity: velocity)
|
||||
return springTiming
|
||||
}
|
||||
|
||||
private func getDamping(with velocity: CGVector) -> CGFloat {
|
||||
let dy = abs(velocity.dy)
|
||||
if dy > velocityThreshold {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/09/18.
|
||||
// Copyright © 2018 scenee. All rights reserved.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
@@ -12,7 +12,14 @@ public protocol FloatingPanelControllerDelegate: class {
|
||||
// if it returns nil, FloatingPanelController uses the default behavior
|
||||
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior?
|
||||
|
||||
func floatingPanelDidMove(_ vc: FloatingPanelController) // any offset changes
|
||||
/// Called when the floating panel has changed to a new position. Can be called inside an animation block, so any
|
||||
/// view properties set inside this function will be automatically animated alongside the panel.
|
||||
func floatingPanelDidChangePosition(_ vc: FloatingPanelController)
|
||||
|
||||
/// Asks the delegate if dragging should begin by the pan gesture recognizer.
|
||||
func floatingPanelShouldBeginDragging(_ vc: FloatingPanelController) -> Bool
|
||||
|
||||
func floatingPanelDidMove(_ vc: FloatingPanelController) // any surface frame changes in dragging
|
||||
|
||||
// called on start of dragging (may require some time and or distance to move)
|
||||
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController)
|
||||
@@ -20,6 +27,16 @@ public protocol FloatingPanelControllerDelegate: class {
|
||||
func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition)
|
||||
func floatingPanelWillBeginDecelerating(_ vc: FloatingPanelController) // called on finger up as we are moving
|
||||
func floatingPanelDidEndDecelerating(_ vc: FloatingPanelController) // called when scroll view grinds to a halt
|
||||
|
||||
// called on start of dragging to remove its views from a parent view controller
|
||||
func floatingPanelDidEndDraggingToRemove(_ vc: FloatingPanelController, withVelocity velocity: CGPoint)
|
||||
// called when its views are removed from a parent view controller
|
||||
func floatingPanelDidEndRemove(_ vc: FloatingPanelController)
|
||||
|
||||
/// Asks the delegate if the other gesture recognizer should be allowed to recognize the gesture in parallel.
|
||||
///
|
||||
/// By default, any tap and long gesture recognizers are allowed to recognize gestures simultaneously.
|
||||
func floatingPanel(_ vc: FloatingPanelController, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool
|
||||
}
|
||||
|
||||
public extension FloatingPanelControllerDelegate {
|
||||
@@ -29,30 +46,94 @@ public extension FloatingPanelControllerDelegate {
|
||||
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
|
||||
return nil
|
||||
}
|
||||
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {}
|
||||
func floatingPanelShouldBeginDragging(_ vc: FloatingPanelController) -> Bool {
|
||||
return true
|
||||
}
|
||||
func floatingPanelDidMove(_ vc: FloatingPanelController) {}
|
||||
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) {}
|
||||
func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) {}
|
||||
func floatingPanelWillBeginDecelerating(_ vc: FloatingPanelController) {}
|
||||
func floatingPanelDidEndDecelerating(_ vc: FloatingPanelController) {}
|
||||
|
||||
func floatingPanelDidEndDraggingToRemove(_ vc: FloatingPanelController, withVelocity velocity: CGPoint) {}
|
||||
func floatingPanelDidEndRemove(_ vc: FloatingPanelController) {}
|
||||
|
||||
func floatingPanel(_ vc: FloatingPanelController, shouldRecognizeSimultaneouslyWith gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public enum FloatingPanelPosition: Int {
|
||||
case full
|
||||
case half
|
||||
case tip
|
||||
case hidden
|
||||
|
||||
static var allCases: [FloatingPanelPosition] {
|
||||
return [.full, .half, .tip, .hidden]
|
||||
}
|
||||
|
||||
func next(in positions: [FloatingPanelPosition]) -> FloatingPanelPosition {
|
||||
#if swift(>=4.2)
|
||||
guard
|
||||
let index = positions.firstIndex(of: self),
|
||||
positions.indices.contains(index + 1)
|
||||
else { return self }
|
||||
#else
|
||||
guard
|
||||
let index = positions.index(of: self),
|
||||
positions.indices.contains(index + 1)
|
||||
else { return self }
|
||||
#endif
|
||||
return positions[index + 1]
|
||||
}
|
||||
|
||||
func pre(in positions: [FloatingPanelPosition]) -> FloatingPanelPosition {
|
||||
#if swift(>=4.2)
|
||||
guard
|
||||
let index = positions.firstIndex(of: self),
|
||||
positions.indices.contains(index - 1)
|
||||
else { return self }
|
||||
#else
|
||||
guard
|
||||
let index = positions.index(of: self),
|
||||
positions.indices.contains(index - 1)
|
||||
else { return self }
|
||||
#endif
|
||||
return positions[index - 1]
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// A container view controller to display a floating panel to present contents in parallel as a user wants.
|
||||
///
|
||||
public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate {
|
||||
open class FloatingPanelController: UIViewController {
|
||||
/// Constants indicating how safe area insets are added to the adjusted content inset.
|
||||
public enum ContentInsetAdjustmentBehavior: Int {
|
||||
case always
|
||||
case never
|
||||
}
|
||||
|
||||
/// A flag used to determine how the controller object lays out the content view when the surface position changes.
|
||||
public enum ContentMode: Int {
|
||||
/// The option to fix the content to keep the height of the top most position.
|
||||
case `static`
|
||||
/// The option to scale the content to fit the bounds of the root view by changing the surface position.
|
||||
case fitToBounds
|
||||
}
|
||||
|
||||
/// The delegate of the floating panel controller object.
|
||||
public weak var delegate: FloatingPanelControllerDelegate?
|
||||
public weak var delegate: FloatingPanelControllerDelegate?{
|
||||
didSet{
|
||||
didUpdateDelegate()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the surface view managed by the controller object. It's the same as `self.view`.
|
||||
public var surfaceView: FloatingPanelSurfaceView! {
|
||||
return view as? FloatingPanelSurfaceView
|
||||
return floatingPanel.surfaceView
|
||||
}
|
||||
|
||||
/// Returns the backdrop view managed by the controller object.
|
||||
@@ -60,76 +141,195 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
return floatingPanel.backdropView
|
||||
}
|
||||
|
||||
/// Returns the scroll view that the conroller tracks.
|
||||
/// Returns the scroll view that the controller tracks.
|
||||
public weak var scrollView: UIScrollView? {
|
||||
return floatingPanel.scrollView
|
||||
}
|
||||
|
||||
// The underlying gesture recognizer for pan gestures
|
||||
public var panGestureRecognizer: UIPanGestureRecognizer {
|
||||
return floatingPanel.panGestureRecognizer
|
||||
}
|
||||
|
||||
/// The current position of the floating panel controller's contents.
|
||||
public var position: FloatingPanelPosition {
|
||||
return floatingPanel.state
|
||||
}
|
||||
|
||||
private var floatingPanel: FloatingPanel!
|
||||
/// The layout object managed by the controller
|
||||
public var layout: FloatingPanelLayout {
|
||||
return floatingPanel.layoutAdapter.layout
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
/// The behavior object managed by the controller
|
||||
public var behavior: FloatingPanelBehavior {
|
||||
return floatingPanel.behavior
|
||||
}
|
||||
|
||||
/// The content insets of the tracking scroll view derived from this safe area
|
||||
public var adjustedContentInsets: UIEdgeInsets {
|
||||
return floatingPanel.layoutAdapter.adjustedContentInsets
|
||||
}
|
||||
|
||||
/// The behavior for determining the adjusted content offsets.
|
||||
///
|
||||
/// This property specifies how the content area of the tracking scroll view is modified using `adjustedContentInsets`. The default value of this property is FloatingPanelController.ContentInsetAdjustmentBehavior.always.
|
||||
public var contentInsetAdjustmentBehavior: ContentInsetAdjustmentBehavior = .always
|
||||
|
||||
/// A Boolean value that determines whether the removal interaction is enabled.
|
||||
public var isRemovalInteractionEnabled: Bool {
|
||||
set { floatingPanel.isRemovalInteractionEnabled = newValue }
|
||||
get { return floatingPanel.isRemovalInteractionEnabled }
|
||||
}
|
||||
|
||||
/// The view controller responsible for the content portion of the floating panel.
|
||||
public var contentViewController: UIViewController? {
|
||||
set { set(contentViewController: newValue) }
|
||||
get { return _contentViewController }
|
||||
}
|
||||
|
||||
/// The NearbyPosition determines that finger's nearby position.
|
||||
public var nearbyPosition: FloatingPanelPosition {
|
||||
let currentY = surfaceView.frame.minY
|
||||
return floatingPanel.targetPosition(from: currentY, with: .zero)
|
||||
}
|
||||
|
||||
public var contentMode: ContentMode = .static {
|
||||
didSet {
|
||||
guard position != .hidden else { return }
|
||||
activateLayout()
|
||||
}
|
||||
}
|
||||
|
||||
private var _contentViewController: UIViewController?
|
||||
|
||||
private(set) var floatingPanel: FloatingPanelCore!
|
||||
private var preSafeAreaInsets: UIEdgeInsets = .zero // Capture the latest one
|
||||
private var safeAreaInsetsObservation: NSKeyValueObservation?
|
||||
private let modalTransition = FloatingPanelModalTransition()
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
setUp()
|
||||
}
|
||||
|
||||
/// Initialize a newly created a floating panel controller.
|
||||
public init() {
|
||||
/// Initialize a newly created floating panel controller.
|
||||
public init(delegate: FloatingPanelControllerDelegate? = nil) {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
self.delegate = delegate
|
||||
setUp()
|
||||
}
|
||||
|
||||
private func setUp() {
|
||||
_ = FloatingPanelController.dismissSwizzling
|
||||
|
||||
modalPresentationStyle = .custom
|
||||
transitioningDelegate = modalTransition
|
||||
|
||||
floatingPanel = FloatingPanelCore(self,
|
||||
layout: fetchLayout(for: self.traitCollection),
|
||||
behavior: fetchBehavior(for: self.traitCollection))
|
||||
}
|
||||
|
||||
private func didUpdateDelegate(){
|
||||
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
|
||||
floatingPanel.behavior = fetchBehavior(for: self.traitCollection)
|
||||
}
|
||||
|
||||
// MARK:- Overrides
|
||||
|
||||
/// Creates the view that the controller manages.
|
||||
override public func loadView() {
|
||||
open override func loadView() {
|
||||
assert(self.storyboard == nil, "Storyboard isn't supported")
|
||||
|
||||
let view = FloatingPanelSurfaceView()
|
||||
view.backgroundColor = .white
|
||||
let view = FloatingPanelPassThroughView()
|
||||
view.backgroundColor = .clear
|
||||
|
||||
backdropView.frame = view.bounds
|
||||
view.addSubview(backdropView)
|
||||
|
||||
surfaceView.frame = view.bounds
|
||||
view.addSubview(surfaceView)
|
||||
|
||||
self.view = view as UIView
|
||||
|
||||
let layout = fetchLayout(for: self.traitCollection)
|
||||
let behavior = fetchBehavior(for: self.traitCollection)
|
||||
floatingPanel = FloatingPanel(self,
|
||||
layout: layout,
|
||||
behavior: behavior)
|
||||
}
|
||||
|
||||
public override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
open override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
if #available(iOS 11.0, *) {}
|
||||
else {
|
||||
// Because {top,bottom}LayoutGuide is managed as a view
|
||||
if preSafeAreaInsets != layoutInsets,
|
||||
floatingPanel.isDecelerating == false {
|
||||
self.update(safeAreaInsets: layoutInsets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
|
||||
if view.translatesAutoresizingMaskIntoConstraints {
|
||||
view.frame.size = size
|
||||
view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
open override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.willTransition(to: newCollection, with: coordinator)
|
||||
self.prepare(for: newCollection)
|
||||
}
|
||||
|
||||
// Change layout for a new trait collection
|
||||
floatingPanel.layoutAdapter.layout = fetchLayout(for: newCollection)
|
||||
open override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
safeAreaInsetsObservation = nil
|
||||
}
|
||||
|
||||
// MARK:- Child view controller to consult
|
||||
#if swift(>=4.2)
|
||||
open override var childForStatusBarStyle: UIViewController? {
|
||||
return contentViewController
|
||||
}
|
||||
|
||||
open override var childForStatusBarHidden: UIViewController? {
|
||||
return contentViewController
|
||||
}
|
||||
|
||||
open override var childForScreenEdgesDeferringSystemGestures: UIViewController? {
|
||||
return contentViewController
|
||||
}
|
||||
|
||||
open override var childForHomeIndicatorAutoHidden: UIViewController? {
|
||||
return contentViewController
|
||||
}
|
||||
#else
|
||||
open override var childViewControllerForStatusBarStyle: UIViewController? {
|
||||
return contentViewController
|
||||
}
|
||||
|
||||
open override var childViewControllerForStatusBarHidden: UIViewController? {
|
||||
return contentViewController
|
||||
}
|
||||
|
||||
open override func childViewControllerForScreenEdgesDeferringSystemGestures() -> UIViewController? {
|
||||
return contentViewController
|
||||
}
|
||||
|
||||
open override func childViewControllerForHomeIndicatorAutoHidden() -> UIViewController? {
|
||||
return contentViewController
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK:- Internals
|
||||
func prepare(for newCollection: UITraitCollection) {
|
||||
guard newCollection.shouldUpdateLayout(from: traitCollection) else { return }
|
||||
// Change a layout & behavior for a new trait collection
|
||||
reloadLayout(for: newCollection)
|
||||
activateLayout()
|
||||
floatingPanel.behavior = fetchBehavior(for: newCollection)
|
||||
|
||||
guard let parent = parent else { fatalError() }
|
||||
|
||||
floatingPanel.layoutAdapter.prepareLayout(toParent: parent)
|
||||
floatingPanel.layoutAdapter.activateLayout(of: floatingPanel.state)
|
||||
}
|
||||
|
||||
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
guard previousTraitCollection != traitCollection else { return }
|
||||
|
||||
if let parent = parent {
|
||||
floatingPanel.safeAreaInsets = parent.layoutInsets
|
||||
}
|
||||
floatingPanel.layoutAdapter.updateHeight()
|
||||
floatingPanel.backdropView.isHidden = (traitCollection.verticalSizeClass == .compact)
|
||||
}
|
||||
|
||||
public override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
// Must set `parent.view.layoutInsets` to floatingPanel.safeAreaInsets` here
|
||||
// because it ensures that parent.view.layoutInsets` has a correct value.
|
||||
if let parent = parent {
|
||||
floatingPanel.safeAreaInsets = parent.layoutInsets
|
||||
floatingPanel.backdropView.frame = parent.view.bounds
|
||||
}
|
||||
}
|
||||
// MARK:- Privates
|
||||
|
||||
private func fetchLayout(for traitCollection: UITraitCollection) -> FloatingPanelLayout {
|
||||
switch traitCollection.verticalSizeClass {
|
||||
@@ -144,53 +344,161 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
return self.delegate?.floatingPanel(self, behaviorFor: traitCollection) ?? FloatingPanelDefaultBehavior()
|
||||
}
|
||||
|
||||
// MARK: Container view controller responsibilities
|
||||
private func update(safeAreaInsets: UIEdgeInsets) {
|
||||
guard
|
||||
preSafeAreaInsets != safeAreaInsets
|
||||
else { return }
|
||||
|
||||
/// Adds the controller as a child of the specified view controller.
|
||||
log.debug("Update safeAreaInsets", safeAreaInsets)
|
||||
|
||||
// Prevent an infinite loop on iOS 10: setUpLayout() -> viewDidLayoutSubviews() -> setUpLayout()
|
||||
preSafeAreaInsets = safeAreaInsets
|
||||
|
||||
activateLayout()
|
||||
|
||||
switch contentInsetAdjustmentBehavior {
|
||||
case .always:
|
||||
scrollView?.contentInset = adjustedContentInsets
|
||||
scrollView?.scrollIndicatorInsets = adjustedContentInsets
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadLayout(for traitCollection: UITraitCollection) {
|
||||
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
|
||||
|
||||
if let parent = self.parent {
|
||||
if let layout = layout as? UIViewController, layout == parent {
|
||||
log.warning("A memory leak will occur by a retain cycle because \(self) owns the parent view controller(\(parent)) as the layout object. Don't let the parent adopt FloatingPanelLayout.")
|
||||
}
|
||||
if let behavior = behavior as? UIViewController, behavior == parent {
|
||||
log.warning("A memory leak will occur by a retain cycle because \(self) owns the parent view controller(\(parent)) as the behavior object. Don't let the parent adopt FloatingPanelBehavior.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func activateLayout() {
|
||||
floatingPanel.layoutAdapter.prepareLayout(in: self)
|
||||
|
||||
// preserve the current content offset
|
||||
let contentOffset = scrollView?.contentOffset
|
||||
|
||||
floatingPanel.layoutAdapter.updateHeight()
|
||||
floatingPanel.layoutAdapter.activateLayout(of: floatingPanel.state)
|
||||
|
||||
scrollView?.contentOffset = contentOffset ?? .zero
|
||||
}
|
||||
|
||||
// MARK: - Container view controller interface
|
||||
|
||||
/// Shows the surface view at the initial position defined by the current layout
|
||||
public func show(animated: Bool = false, completion: (() -> Void)? = nil) {
|
||||
// Must apply the current layout here
|
||||
reloadLayout(for: traitCollection)
|
||||
activateLayout()
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
// Must track the safeAreaInsets of `self.view` to update the layout.
|
||||
// There are 2 reasons.
|
||||
// 1. This or the parent VC doesn't call viewSafeAreaInsetsDidChange() on the bottom
|
||||
// inset's update expectedly.
|
||||
// 2. The safe area top inset can be variable on the large title navigation bar(iOS11+).
|
||||
// That's why it needs the observation to keep `adjustedContentInsets` correct.
|
||||
safeAreaInsetsObservation = self.observe(\.view.safeAreaInsets, options: [.initial, .new, .old]) { [weak self] (vc, change) in
|
||||
guard change.oldValue != change.newValue else { return }
|
||||
self?.update(safeAreaInsets: vc.layoutInsets)
|
||||
}
|
||||
} else {
|
||||
// KVOs for topLayoutGuide & bottomLayoutGuide are not effective.
|
||||
// Instead, update(safeAreaInsets:) is called at `viewDidLayoutSubviews()`
|
||||
}
|
||||
|
||||
move(to: floatingPanel.layoutAdapter.layout.initialPosition,
|
||||
animated: animated,
|
||||
completion: completion)
|
||||
}
|
||||
|
||||
/// Hides the surface view to the hidden position
|
||||
public func hide(animated: Bool = false, completion: (() -> Void)? = nil) {
|
||||
move(to: .hidden,
|
||||
animated: animated,
|
||||
completion: completion)
|
||||
}
|
||||
|
||||
/// Adds the view managed by the controller as a child of the specified view controller.
|
||||
/// - Parameters:
|
||||
/// - parent: A parent view controller object that displays FloatingPanelController's view. A conatiner view controller object isn't applicable.
|
||||
/// - belowView: Insert the surface view managed by the controller below the specified view. As default, the surface view will be added to the end of the parent list of subviews.
|
||||
/// - parent: A parent view controller object that displays FloatingPanelController's view. A container view controller object isn't applicable.
|
||||
/// - belowView: Insert the surface view managed by the controller below the specified view. By default, the surface view will be added to the end of the parent list of subviews.
|
||||
/// - animated: Pass true to animate the presentation; otherwise, pass false.
|
||||
public func add(toParent parent: UIViewController, belowView: UIView? = nil, animated: Bool = false) {
|
||||
public func addPanel(toParent parent: UIViewController, belowView: UIView? = nil, animated: Bool = false) {
|
||||
guard self.parent == nil else {
|
||||
log.warning("Already added to a parent(\(parent))")
|
||||
return
|
||||
}
|
||||
precondition((parent is UINavigationController) == false, "UINavigationController displays only one child view controller at a time.")
|
||||
precondition((parent is UITableViewController) == false, "UITableViewController should not be the parent because the view hierarchy will be break in reusing cells.")
|
||||
precondition((parent is UICollectionViewController) == false, "UICollectionViewController should not be the parent because the view hierarchy will be break in reusing cells.")
|
||||
precondition((parent is UITabBarController) == false, "UITabBarController displays child view controllers with a radio-style selection interface")
|
||||
precondition((parent is UISplitViewController) == false, "UISplitViewController manages two child view controllers in a master-detail interface")
|
||||
precondition((parent is UITableViewController) == false, "UITableViewController should not be the parent because the view is a table view so that a floating panel doens't work well")
|
||||
precondition((parent is UICollectionViewController) == false, "UICollectionViewController should not be the parent because the view is a collection view so that a floating panel doens't work well")
|
||||
|
||||
view.frame = parent.view.bounds
|
||||
if let belowView = belowView {
|
||||
parent.view.insertSubview(self.view, belowSubview: belowView)
|
||||
} else {
|
||||
parent.view.addSubview(self.view)
|
||||
}
|
||||
|
||||
#if swift(>=4.2)
|
||||
parent.addChild(self)
|
||||
#else
|
||||
parent.addChildViewController(self)
|
||||
#endif
|
||||
|
||||
// Must set a layout again here because `self.traitCollection` is applied correctly on it's added to a parent VC
|
||||
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
|
||||
floatingPanel.layoutViews(in: parent)
|
||||
floatingPanel.present(animated: animated) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
view.frame = parent.view.bounds // Needed for a correct safe area configuration
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
self.view.topAnchor.constraint(equalTo: parent.view.topAnchor, constant: 0.0),
|
||||
self.view.leftAnchor.constraint(equalTo: parent.view.leftAnchor, constant: 0.0),
|
||||
self.view.rightAnchor.constraint(equalTo: parent.view.rightAnchor, constant: 0.0),
|
||||
self.view.bottomAnchor.constraint(equalTo: parent.view.bottomAnchor, constant: 0.0),
|
||||
])
|
||||
|
||||
show(animated: animated) { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
#if swift(>=4.2)
|
||||
self.didMove(toParent: parent)
|
||||
#else
|
||||
self.didMove(toParentViewController: parent)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the view controller from its parent.
|
||||
/// Removes the controller and the managed view from its parent view controller
|
||||
/// - Parameters:
|
||||
/// - animated: Pass true to animate the presentation; otherwise, pass false.
|
||||
/// - completion: The block to execute after the view controller is dismissed. This block has no return value and takes no parameters. You may specify nil for this parameter.
|
||||
public func removeFromParent(animated: Bool = false, completion: (() -> Void)? = nil) {
|
||||
guard self.parent != nil else { return }
|
||||
|
||||
floatingPanel.dismiss(animated: animated) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
public func removePanelFromParent(animated: Bool, completion: (() -> Void)? = nil) {
|
||||
guard self.parent != nil else {
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
|
||||
hide(animated: animated) { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
#if swift(>=4.2)
|
||||
self.willMove(toParent: nil)
|
||||
#else
|
||||
self.willMove(toParentViewController: nil)
|
||||
#endif
|
||||
|
||||
self.view.removeFromSuperview()
|
||||
|
||||
#if swift(>=4.2)
|
||||
self.removeFromParent()
|
||||
#else
|
||||
self.removeFromParentViewController()
|
||||
#endif
|
||||
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
@@ -199,41 +507,160 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
/// - Parameters:
|
||||
/// - to: Pass a FloatingPanelPosition value to move the surface view to the position.
|
||||
/// - animated: Pass true to animate the presentation; otherwise, pass false.
|
||||
/// - completion: The block to execute after the view controller is dismissed. This block has no return value and takes no parameters. You may specify nil for this parameter.
|
||||
/// - completion: The block to execute after the view controller has finished moving. This block has no return value and takes no parameters. You may specify nil for this parameter.
|
||||
public func move(to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
|
||||
precondition(floatingPanel.layoutAdapter.vc != nil, "Use show(animated:completion)")
|
||||
floatingPanel.move(to: to, animated: animated, completion: completion)
|
||||
}
|
||||
|
||||
/// Presents the specified view controller as the content view controller in the surface view interface.
|
||||
public override func show(_ vc: UIViewController, sender: Any?) {
|
||||
let surfaceView = self.view as! FloatingPanelSurfaceView
|
||||
surfaceView.contentView.addSubview(vc.view)
|
||||
vc.view.frame = surfaceView.contentView.bounds
|
||||
vc.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
vc.view.topAnchor.constraint(equalTo: surfaceView.contentView.topAnchor, constant: 0.0),
|
||||
vc.view.leftAnchor.constraint(equalTo: surfaceView.contentView.leftAnchor, constant: 0.0),
|
||||
vc.view.rightAnchor.constraint(equalTo: surfaceView.contentView.rightAnchor, constant: 0.0),
|
||||
vc.view.bottomAnchor.constraint(equalTo: surfaceView.contentView.bottomAnchor, constant: 0.0),
|
||||
])
|
||||
addChild(vc)
|
||||
vc.didMove(toParent: self)
|
||||
/// Sets the view controller responsible for the content portion of the floating panel.
|
||||
public func set(contentViewController: UIViewController?) {
|
||||
if let vc = _contentViewController {
|
||||
#if swift(>=4.2)
|
||||
vc.willMove(toParent: nil)
|
||||
#else
|
||||
vc.willMove(toParentViewController: nil)
|
||||
#endif
|
||||
|
||||
vc.view.removeFromSuperview()
|
||||
|
||||
#if swift(>=4.2)
|
||||
vc.removeFromParent()
|
||||
#else
|
||||
vc.removeFromParentViewController()
|
||||
#endif
|
||||
}
|
||||
|
||||
if let vc = contentViewController {
|
||||
#if swift(>=4.2)
|
||||
addChild(vc)
|
||||
#else
|
||||
addChildViewController(vc)
|
||||
#endif
|
||||
|
||||
let surfaceView = floatingPanel.surfaceView
|
||||
surfaceView.add(contentView: vc.view)
|
||||
|
||||
#if swift(>=4.2)
|
||||
vc.didMove(toParent: self)
|
||||
#else
|
||||
vc.didMove(toParentViewController: self)
|
||||
#endif
|
||||
}
|
||||
|
||||
_contentViewController = contentViewController
|
||||
}
|
||||
|
||||
/// Tracks the specified scroll view for the inteface to correspond with the scroll pan gesture.
|
||||
public func track(scrollView: UIScrollView) {
|
||||
floatingPanel.scrollView = scrollView
|
||||
}
|
||||
|
||||
/// Returns the y-coordinate of the point at the origin of the surface view
|
||||
public func originYOfSurface(for pos: FloatingPanelPosition) -> CGFloat {
|
||||
switch pos {
|
||||
case .full:
|
||||
return floatingPanel.layoutAdapter.topY
|
||||
case .half:
|
||||
return floatingPanel.layoutAdapter.middleY
|
||||
case .tip:
|
||||
return floatingPanel.layoutAdapter.bottomY
|
||||
@available(*, unavailable, renamed: "set(contentViewController:)")
|
||||
open override func show(_ vc: UIViewController, sender: Any?) {
|
||||
if let target = self.parent?.targetViewController(forAction: #selector(UIViewController.show(_:sender:)), sender: sender) {
|
||||
target.show(vc, sender: sender)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable, renamed: "set(contentViewController:)")
|
||||
open override func showDetailViewController(_ vc: UIViewController, sender: Any?) {
|
||||
if let target = self.parent?.targetViewController(forAction: #selector(UIViewController.showDetailViewController(_:sender:)), sender: sender) {
|
||||
target.showDetailViewController(vc, sender: sender)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Scroll view tracking
|
||||
|
||||
/// Tracks the specified scroll view to correspond with the scroll.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - scrollView: Specify a scroll view to continuously and seamlessly work in concert with interactions of the surface view or nil to cancel it.
|
||||
public func track(scrollView: UIScrollView?) {
|
||||
guard let scrollView = scrollView else {
|
||||
floatingPanel.scrollView = nil
|
||||
return
|
||||
}
|
||||
|
||||
floatingPanel.scrollView = scrollView
|
||||
|
||||
switch contentInsetAdjustmentBehavior {
|
||||
case .always:
|
||||
if #available(iOS 11.0, *) {
|
||||
scrollView.contentInsetAdjustmentBehavior = .never
|
||||
} else {
|
||||
#if swift(>=4.2)
|
||||
children.forEach { (vc) in
|
||||
vc.automaticallyAdjustsScrollViewInsets = false
|
||||
}
|
||||
#else
|
||||
childViewControllers.forEach { (vc) in
|
||||
vc.automaticallyAdjustsScrollViewInsets = false
|
||||
}
|
||||
#endif
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Utilities
|
||||
|
||||
/// Updates the layout object from the delegate and lays out the views managed
|
||||
/// by the controller immediately.
|
||||
///
|
||||
/// This method updates the `FloatingPanelLayout` object from the delegate and
|
||||
/// then it calls `layoutIfNeeded()` of the root view to force the view
|
||||
/// to update the floating panel's layout immediately. It can be called in an
|
||||
/// animation block.
|
||||
public func updateLayout() {
|
||||
reloadLayout(for: traitCollection)
|
||||
activateLayout()
|
||||
}
|
||||
|
||||
/// Returns the y-coordinate of the point at the origin of the surface view.
|
||||
public func originYOfSurface(for pos: FloatingPanelPosition) -> CGFloat {
|
||||
return floatingPanel.layoutAdapter.positionY(for: pos)
|
||||
}
|
||||
}
|
||||
|
||||
extension FloatingPanelController {
|
||||
private static let dismissSwizzling: Any? = {
|
||||
let aClass: AnyClass! = UIViewController.self //object_getClass(vc)
|
||||
if let imp = class_getMethodImplementation(aClass, #selector(dismiss(animated:completion:))),
|
||||
let originalAltMethod = class_getInstanceMethod(aClass, #selector(fp_original_dismiss(animated:completion:))) {
|
||||
method_setImplementation(originalAltMethod, imp)
|
||||
}
|
||||
let originalMethod = class_getInstanceMethod(aClass, #selector(dismiss(animated:completion:)))
|
||||
let swizzledMethod = class_getInstanceMethod(aClass, #selector(fp_dismiss(animated:completion:)))
|
||||
if let originalMethod = originalMethod, let swizzledMethod = swizzledMethod {
|
||||
// switch implementation..
|
||||
method_exchangeImplementations(originalMethod, swizzledMethod)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
}
|
||||
|
||||
public extension UIViewController {
|
||||
@objc func fp_original_dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
||||
// Implementation will be replaced by IMP of self.dismiss(animated:completion:)
|
||||
}
|
||||
@objc func fp_dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
||||
// Call dismiss(animated:completion:) to a content view controller
|
||||
if let fpc = parent as? FloatingPanelController {
|
||||
if fpc.presentingViewController != nil {
|
||||
self.fp_original_dismiss(animated: flag, completion: completion)
|
||||
} else {
|
||||
fpc.removePanelFromParent(animated: flag, completion: completion)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Call dismiss(animated:completion:) to FloatingPanelController directly
|
||||
if let fpc = self as? FloatingPanelController {
|
||||
if fpc.presentingViewController != nil {
|
||||
self.fp_original_dismiss(animated: flag, completion: completion)
|
||||
} else {
|
||||
fpc.removePanelFromParent(animated: flag, completion: completion)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// For other view controllers
|
||||
self.fp_original_dismiss(animated: flag, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,923 @@
|
||||
//
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import UIKit.UIGestureRecognizerSubclass // For Xcode 9.4.1
|
||||
|
||||
///
|
||||
/// FloatingPanel presentation model
|
||||
///
|
||||
class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
|
||||
// MUST be a weak reference to prevent UI freeze on the presentation modally
|
||||
weak var viewcontroller: FloatingPanelController?
|
||||
|
||||
let surfaceView: FloatingPanelSurfaceView
|
||||
let backdropView: FloatingPanelBackdropView
|
||||
var layoutAdapter: FloatingPanelLayoutAdapter
|
||||
var behavior: FloatingPanelBehavior
|
||||
|
||||
weak var scrollView: UIScrollView? {
|
||||
didSet {
|
||||
oldValue?.panGestureRecognizer.removeTarget(self, action: nil)
|
||||
scrollView?.panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:)))
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var state: FloatingPanelPosition = .hidden {
|
||||
didSet {
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidChangePosition(vc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isBottomState: Bool {
|
||||
let remains = layoutAdapter.supportedPositions.filter { $0.rawValue > state.rawValue }
|
||||
return remains.count == 0
|
||||
}
|
||||
|
||||
let panGestureRecognizer: FloatingPanelPanGestureRecognizer
|
||||
var isRemovalInteractionEnabled: Bool = false
|
||||
|
||||
fileprivate var animator: UIViewPropertyAnimator?
|
||||
|
||||
private var initialFrame: CGRect = .zero
|
||||
private var initialTranslationY: CGFloat = 0
|
||||
private var initialLocation: CGPoint = .nan
|
||||
|
||||
var interactionInProgress: Bool = false
|
||||
var isDecelerating: Bool = false
|
||||
|
||||
// Scroll handling
|
||||
private var initialScrollOffset: CGPoint = .zero
|
||||
private var stopScrollDeceleration: Bool = false
|
||||
private var scrollBouncable = false
|
||||
private var scrollIndictorVisible = false
|
||||
|
||||
// MARK: - Interface
|
||||
|
||||
init(_ vc: FloatingPanelController, layout: FloatingPanelLayout, behavior: FloatingPanelBehavior) {
|
||||
viewcontroller = vc
|
||||
|
||||
surfaceView = FloatingPanelSurfaceView()
|
||||
surfaceView.backgroundColor = .white
|
||||
|
||||
backdropView = FloatingPanelBackdropView()
|
||||
backdropView.backgroundColor = .black
|
||||
backdropView.alpha = 0.0
|
||||
|
||||
self.layoutAdapter = FloatingPanelLayoutAdapter(surfaceView: surfaceView,
|
||||
backdropView: backdropView,
|
||||
layout: layout)
|
||||
self.behavior = behavior
|
||||
|
||||
panGestureRecognizer = FloatingPanelPanGestureRecognizer()
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
panGestureRecognizer.name = "FloatingPanelSurface"
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
panGestureRecognizer.floatingPanel = self
|
||||
surfaceView.addGestureRecognizer(panGestureRecognizer)
|
||||
panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:)))
|
||||
panGestureRecognizer.delegate = self
|
||||
|
||||
// Set tap-to-dismiss in the backdrop view
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
|
||||
tapGesture.isEnabled = false
|
||||
backdropView.dismissalTapGestureRecognizer = tapGesture
|
||||
backdropView.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
func move(to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
|
||||
move(from: state, to: to, animated: animated, completion: completion)
|
||||
}
|
||||
|
||||
private func move(from: FloatingPanelPosition, to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
|
||||
assert(layoutAdapter.isValid(to), "Can't move to '\(to)' position because it's not valid in the layout")
|
||||
guard let vc = viewcontroller else {
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
if state != layoutAdapter.topMostState {
|
||||
lockScrollView()
|
||||
}
|
||||
tearDownActiveInteraction()
|
||||
|
||||
if animated {
|
||||
let animator: UIViewPropertyAnimator
|
||||
switch (from, to) {
|
||||
case (.hidden, let to):
|
||||
animator = behavior.addAnimator(vc, to: to)
|
||||
case (let from, .hidden):
|
||||
animator = behavior.removeAnimator(vc, from: from)
|
||||
case (let from, let to):
|
||||
animator = behavior.moveAnimator(vc, from: from, to: to)
|
||||
}
|
||||
|
||||
animator.addAnimations { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
|
||||
self.state = to
|
||||
self.updateLayout(to: to)
|
||||
}
|
||||
animator.addCompletion { [weak self] _ in
|
||||
guard let `self` = self else { return }
|
||||
self.animator = nil
|
||||
if self.state == self.layoutAdapter.topMostState {
|
||||
self.unlockScrollView()
|
||||
} else {
|
||||
self.lockScrollView()
|
||||
}
|
||||
completion?()
|
||||
}
|
||||
self.animator = animator
|
||||
animator.startAnimation()
|
||||
} else {
|
||||
self.state = to
|
||||
self.updateLayout(to: to)
|
||||
if self.state == self.layoutAdapter.topMostState {
|
||||
self.unlockScrollView()
|
||||
} else {
|
||||
self.lockScrollView()
|
||||
}
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Layout update
|
||||
|
||||
private func updateLayout(to target: FloatingPanelPosition) {
|
||||
self.layoutAdapter.activateFixedLayout()
|
||||
self.layoutAdapter.activateInteractiveLayout(of: target)
|
||||
}
|
||||
|
||||
func getBackdropAlpha(at currentY: CGFloat, with translation: CGPoint) -> CGFloat {
|
||||
let forwardY = (translation.y >= 0)
|
||||
let segment = layoutAdapter.segument(at: currentY, forward: forwardY)
|
||||
let lowerPos = segment.lower ?? layoutAdapter.topMostState
|
||||
let upperPos = segment.upper ?? layoutAdapter.bottomMostState
|
||||
|
||||
let pre = forwardY ? lowerPos : upperPos
|
||||
let next = forwardY ? upperPos : lowerPos
|
||||
|
||||
let nextY = layoutAdapter.positionY(for: next)
|
||||
let preY = layoutAdapter.positionY(for: pre)
|
||||
|
||||
let nextAlpha = layoutAdapter.layout.backdropAlphaFor(position: next)
|
||||
let preAlpha = layoutAdapter.layout.backdropAlphaFor(position: pre)
|
||||
|
||||
if preY == nextY {
|
||||
return preAlpha
|
||||
} else {
|
||||
return preAlpha + max(min(1.0, 1.0 - (nextY - currentY) / (nextY - preY) ), 0.0) * (nextAlpha - preAlpha)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIGestureRecognizerDelegate
|
||||
|
||||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
|
||||
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
guard gestureRecognizer == panGestureRecognizer else { return false }
|
||||
|
||||
/* log.debug("shouldRecognizeSimultaneouslyWith", otherGestureRecognizer) */
|
||||
|
||||
if let vc = viewcontroller,
|
||||
vc.delegate?.floatingPanel(vc, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
|
||||
return true
|
||||
}
|
||||
|
||||
switch otherGestureRecognizer {
|
||||
case is UIPanGestureRecognizer,
|
||||
is UISwipeGestureRecognizer,
|
||||
is UIRotationGestureRecognizer,
|
||||
is UIScreenEdgePanGestureRecognizer,
|
||||
is UIPinchGestureRecognizer:
|
||||
// all gestures of the tracking scroll view should be recognized in parallel
|
||||
// and handle them in self.handle(panGesture:)
|
||||
return scrollView?.gestureRecognizers?.contains(otherGestureRecognizer) ?? false
|
||||
default:
|
||||
// Should recognize tap/long press gestures in parallel when the surface view is at an anchor position.
|
||||
let surfaceFrame = surfaceView.layer.presentation()?.frame ?? surfaceView.frame
|
||||
let surfaceY = surfaceFrame.minY
|
||||
let adapterY = layoutAdapter.positionY(for: state)
|
||||
|
||||
return abs(surfaceY - adapterY) < (1.0 / surfaceView.traitCollection.displayScale)
|
||||
}
|
||||
}
|
||||
|
||||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
guard gestureRecognizer == panGestureRecognizer else { return false }
|
||||
/* log.debug("shouldBeRequiredToFailBy", otherGestureRecognizer) */
|
||||
return false
|
||||
}
|
||||
|
||||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
guard gestureRecognizer == panGestureRecognizer else { return false }
|
||||
|
||||
/* log.debug("shouldRequireFailureOf", otherGestureRecognizer) */
|
||||
|
||||
// Should begin the pan gesture without waiting for the tracking scroll view's gestures.
|
||||
// `scrollView.gestureRecognizers` can contains the following gestures
|
||||
// * UIScrollViewDelayedTouchesBeganGestureRecognizer
|
||||
// * UIScrollViewPanGestureRecognizer (scrollView.panGestureRecognizer)
|
||||
// * _UIDragAutoScrollGestureRecognizer
|
||||
// * _UISwipeActionPanGestureRecognizer
|
||||
// * UISwipeDismissalGestureRecognizer
|
||||
if let scrollView = scrollView {
|
||||
// On short contents scroll, `_UISwipeActionPanGestureRecognizer` blocks
|
||||
// the panel's pan gesture if not returns false
|
||||
if let scrollGestureRecognizers = scrollView.gestureRecognizers,
|
||||
scrollGestureRecognizers.contains(otherGestureRecognizer) {
|
||||
switch otherGestureRecognizer {
|
||||
case scrollView.panGestureRecognizer:
|
||||
if grabberAreaFrame.contains(gestureRecognizer.location(in: gestureRecognizer.view)) {
|
||||
return false
|
||||
}
|
||||
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
|
||||
return allowScrollPanGesture(at: CGPoint(x: 0.0, y: offset))
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let vc = viewcontroller,
|
||||
vc.delegate?.floatingPanel(vc, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
|
||||
return false
|
||||
}
|
||||
|
||||
switch otherGestureRecognizer {
|
||||
case is UIPanGestureRecognizer,
|
||||
is UISwipeGestureRecognizer,
|
||||
is UIRotationGestureRecognizer,
|
||||
is UIScreenEdgePanGestureRecognizer,
|
||||
is UIPinchGestureRecognizer:
|
||||
// Do not begin the pan gesture until these gestures fail
|
||||
return true
|
||||
default:
|
||||
// Should begin the pan gesture without waiting tap/long press gestures fail
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var grabberAreaFrame: CGRect {
|
||||
let grabberAreaFrame = CGRect(x: surfaceView.bounds.origin.x,
|
||||
y: surfaceView.bounds.origin.y,
|
||||
width: surfaceView.bounds.width,
|
||||
height: surfaceView.topGrabberBarHeight * 2)
|
||||
return grabberAreaFrame
|
||||
}
|
||||
|
||||
// MARK: - Gesture handling
|
||||
|
||||
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
|
||||
viewcontroller?.dismiss(animated: true) { [weak self] in
|
||||
guard let vc = self?.viewcontroller else { return }
|
||||
vc.delegate?.floatingPanelDidEndRemove(vc)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handle(panGesture: UIPanGestureRecognizer) {
|
||||
let velocity = panGesture.velocity(in: panGesture.view)
|
||||
|
||||
switch panGesture {
|
||||
case scrollView?.panGestureRecognizer:
|
||||
guard let scrollView = scrollView else { return }
|
||||
|
||||
let location = panGesture.location(in: surfaceView)
|
||||
|
||||
let surfaceMinY = surfaceView.presentationFrame.minY
|
||||
let adapterTopY = layoutAdapter.topY
|
||||
let belowTop = surfaceMinY > (adapterTopY + (1.0 / surfaceView.traitCollection.displayScale))
|
||||
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
|
||||
|
||||
log.debug("scroll gesture(\(state):\(panGesture.state)) --",
|
||||
"belowTop = \(belowTop),",
|
||||
"interactionInProgress = \(interactionInProgress),",
|
||||
"scroll offset = \(offset),",
|
||||
"location = \(location.y), velocity = \(velocity.y)")
|
||||
|
||||
|
||||
if belowTop {
|
||||
// Scroll offset pinning
|
||||
if state == layoutAdapter.topMostState {
|
||||
if interactionInProgress {
|
||||
log.debug("settle offset --", initialScrollOffset.y)
|
||||
scrollView.setContentOffset(initialScrollOffset, animated: false)
|
||||
} else {
|
||||
if grabberAreaFrame.contains(location) {
|
||||
// Preserve the current content offset in moving from full.
|
||||
scrollView.setContentOffset(initialScrollOffset, animated: false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scrollView.setContentOffset(initialScrollOffset, animated: false)
|
||||
}
|
||||
|
||||
// Hide a scroll indicator at the non-top in dragging.
|
||||
if interactionInProgress {
|
||||
lockScrollView()
|
||||
} else {
|
||||
if state == layoutAdapter.topMostState, self.animator == nil,
|
||||
offset > 0, velocity.y < 0 {
|
||||
unlockScrollView()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if interactionInProgress {
|
||||
// Show a scroll indicator at the top in dragging.
|
||||
if offset >= 0, velocity.y <= 0 {
|
||||
unlockScrollView()
|
||||
} else {
|
||||
if state == layoutAdapter.topMostState {
|
||||
// Adjust a small gap of the scroll offset just after swiping down starts in the grabber area.
|
||||
if grabberAreaFrame.contains(location), grabberAreaFrame.contains(initialLocation) {
|
||||
scrollView.setContentOffset(initialScrollOffset, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if state == layoutAdapter.topMostState {
|
||||
// Hide a scroll indicator just before starting an interaction by swiping a panel down.
|
||||
if velocity.y > 0, !allowScrollPanGesture(at: CGPoint(x: 0.0, y: offset)) {
|
||||
lockScrollView()
|
||||
}
|
||||
// Show a scroll indicator when an animation is interrupted at the top and content is scrolled up
|
||||
if velocity.y < 0, allowScrollPanGesture(at: CGPoint(x: 0.0, y: offset)) {
|
||||
unlockScrollView()
|
||||
}
|
||||
|
||||
// Adjust a small gap of the scroll offset just before swiping down starts in the grabber area,
|
||||
if grabberAreaFrame.contains(location), grabberAreaFrame.contains(initialLocation) {
|
||||
scrollView.setContentOffset(initialScrollOffset, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case panGestureRecognizer:
|
||||
let translation = panGesture.translation(in: panGestureRecognizer.view!.superview)
|
||||
let location = panGesture.location(in: panGesture.view)
|
||||
|
||||
log.debug("panel gesture(\(state):\(panGesture.state)) --",
|
||||
"translation = \(translation.y), location = \(location.y), velocity = \(velocity.y)")
|
||||
|
||||
if interactionInProgress == false, isDecelerating == false,
|
||||
let vc = viewcontroller, vc.delegate?.floatingPanelShouldBeginDragging(vc) == false {
|
||||
return
|
||||
}
|
||||
|
||||
if let animator = self.animator {
|
||||
guard surfaceView.presentationFrame.minY >= layoutAdapter.topMaxY else { return }
|
||||
log.debug("panel animation(interruptible: \(animator.isInterruptible)) interrupted!!!")
|
||||
if animator.isInterruptible {
|
||||
animator.stopAnimation(false)
|
||||
// A user can stop a panel at the nearest Y of a target position so this fine-tunes
|
||||
// the a small gap between the presentation layer frame and model layer frame
|
||||
// to unlock scroll view properly at finishAnimation(at:)
|
||||
if abs(surfaceView.frame.minY - layoutAdapter.topY) <= 1.0 {
|
||||
surfaceView.frame.origin.y = layoutAdapter.topY
|
||||
}
|
||||
animator.finishAnimation(at: .current)
|
||||
} else {
|
||||
self.endAnimation(false) // Must call it manually
|
||||
}
|
||||
}
|
||||
|
||||
if panGesture.state == .began {
|
||||
panningBegan(at: location)
|
||||
return
|
||||
}
|
||||
|
||||
if shouldScrollViewHandleTouch(scrollView, point: location, velocity: velocity) {
|
||||
return
|
||||
}
|
||||
|
||||
switch panGesture.state {
|
||||
case .changed:
|
||||
if interactionInProgress == false {
|
||||
startInteraction(with: translation, at: location)
|
||||
}
|
||||
panningChange(with: translation)
|
||||
case .ended, .cancelled, .failed:
|
||||
if interactionInProgress == false {
|
||||
startInteraction(with: translation, at: location)
|
||||
// Workaround: Prevent stopping the surface view b/w anchors if the pan gesture
|
||||
// doesn't pass through .changed state after an interruptible animator is interrupted.
|
||||
let dy = translation.y - .leastNonzeroMagnitude
|
||||
layoutAdapter.updateInteractiveTopConstraint(diff: dy,
|
||||
allowsTopBuffer: true,
|
||||
with: behavior)
|
||||
}
|
||||
panningEnd(with: translation, velocity: velocity)
|
||||
default:
|
||||
break
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func shouldScrollViewHandleTouch(_ scrollView: UIScrollView?, point: CGPoint, velocity: CGPoint) -> Bool {
|
||||
// When no scrollView, nothing to handle.
|
||||
guard let scrollView = scrollView else { return false }
|
||||
|
||||
// For _UISwipeActionPanGestureRecognizer
|
||||
if let scrollGestureRecognizers = scrollView.gestureRecognizers {
|
||||
for gesture in scrollGestureRecognizers {
|
||||
guard gesture.state == .began || gesture.state == .changed
|
||||
else { continue }
|
||||
|
||||
if gesture != scrollView.panGestureRecognizer {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guard
|
||||
state == layoutAdapter.topMostState, // When not top most(i.e. .full), don't scroll.
|
||||
interactionInProgress == false, // When interaction already in progress, don't scroll.
|
||||
surfaceView.frame.minY == layoutAdapter.topY
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
// When the current and initial point within grabber area, do scroll.
|
||||
if grabberAreaFrame.contains(point), !grabberAreaFrame.contains(initialLocation) {
|
||||
return true
|
||||
}
|
||||
|
||||
guard
|
||||
scrollView.frame.contains(initialLocation), // When initialLocation not in scrollView, don't scroll.
|
||||
!grabberAreaFrame.contains(point) // When point within grabber area, don't scroll.
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
|
||||
// The zero offset must be excluded because the offset is usually zero
|
||||
// after a panel moves from half/tip to full.
|
||||
if offset > 0.0 {
|
||||
return true
|
||||
}
|
||||
if scrollView.isDecelerating {
|
||||
return true
|
||||
}
|
||||
if velocity.y <= 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private func panningBegan(at location: CGPoint) {
|
||||
// A user interaction does not always start from Began state of the pan gesture
|
||||
// because it can be recognized in scrolling a content in a content view controller.
|
||||
// So here just preserve the current state if needed.
|
||||
log.debug("panningBegan -- location = \(location.y)")
|
||||
initialLocation = location
|
||||
|
||||
guard let scrollView = scrollView else { return }
|
||||
if state == layoutAdapter.topMostState {
|
||||
if grabberAreaFrame.contains(location) {
|
||||
initialScrollOffset = scrollView.contentOffset
|
||||
}
|
||||
} else {
|
||||
initialScrollOffset = scrollView.contentOffset
|
||||
}
|
||||
}
|
||||
|
||||
private func panningChange(with translation: CGPoint) {
|
||||
log.debug("panningChange -- translation = \(translation.y)")
|
||||
let preY = surfaceView.frame.minY
|
||||
let dy = translation.y - initialTranslationY
|
||||
|
||||
layoutAdapter.updateInteractiveTopConstraint(diff: dy,
|
||||
allowsTopBuffer: allowsTopBuffer(for: dy),
|
||||
with: behavior)
|
||||
|
||||
let currentY = surfaceView.frame.minY
|
||||
backdropView.alpha = getBackdropAlpha(at: currentY, with: translation)
|
||||
preserveContentVCLayoutIfNeeded()
|
||||
|
||||
let didMove = (preY != currentY)
|
||||
guard didMove else { return }
|
||||
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidMove(vc)
|
||||
}
|
||||
}
|
||||
|
||||
private func allowsTopBuffer(for translationY: CGFloat) -> Bool {
|
||||
let preY = surfaceView.frame.minY
|
||||
let nextY = initialFrame.offsetBy(dx: 0.0, dy: translationY).minY
|
||||
if let scrollView = scrollView, scrollView.panGestureRecognizer.state == .changed,
|
||||
preY > 0 && preY > nextY {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private var disabledBottomAutoLayout = false
|
||||
private var disabledAutoLayoutItems: Set<NSLayoutConstraint> = []
|
||||
// Prevent stretching a view having a constraint to SafeArea.bottom in an overflow
|
||||
// from the full position because SafeArea is global in a screen.
|
||||
private func preserveContentVCLayoutIfNeeded() {
|
||||
guard let vc = viewcontroller else { return }
|
||||
guard vc.contentMode != .fitToBounds else { return }
|
||||
|
||||
// Must include topY
|
||||
if (surfaceView.frame.minY <= layoutAdapter.topY) {
|
||||
if !disabledBottomAutoLayout {
|
||||
disabledAutoLayoutItems.removeAll()
|
||||
vc.contentViewController?.view?.constraints.forEach({ (const) in
|
||||
switch vc.contentViewController?.layoutGuide.bottomAnchor {
|
||||
case const.firstAnchor:
|
||||
(const.secondItem as? UIView)?.disableAutoLayout()
|
||||
const.isActive = false
|
||||
disabledAutoLayoutItems.insert(const)
|
||||
case const.secondAnchor:
|
||||
(const.firstItem as? UIView)?.disableAutoLayout()
|
||||
const.isActive = false
|
||||
disabledAutoLayoutItems.insert(const)
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
disabledBottomAutoLayout = true
|
||||
} else {
|
||||
if disabledBottomAutoLayout {
|
||||
disabledAutoLayoutItems.forEach({ (const) in
|
||||
switch vc.contentViewController?.layoutGuide.bottomAnchor {
|
||||
case const.firstAnchor:
|
||||
(const.secondItem as? UIView)?.enableAutoLayout()
|
||||
const.isActive = true
|
||||
case const.secondAnchor:
|
||||
(const.firstItem as? UIView)?.enableAutoLayout()
|
||||
const.isActive = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
disabledAutoLayoutItems.removeAll()
|
||||
}
|
||||
disabledBottomAutoLayout = false
|
||||
}
|
||||
}
|
||||
|
||||
private func panningEnd(with translation: CGPoint, velocity: CGPoint) {
|
||||
log.debug("panningEnd -- translation = \(translation.y), velocity = \(velocity.y)")
|
||||
|
||||
if state == .hidden {
|
||||
log.debug("Already hidden")
|
||||
return
|
||||
}
|
||||
|
||||
stopScrollDeceleration = surfaceView.frame.minY > (layoutAdapter.topY + (1.0 / surfaceView.traitCollection.displayScale)) // Projecting the dragging to the scroll dragging or not
|
||||
if stopScrollDeceleration {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
self.stopScrollingWithDeceleration(at: self.initialScrollOffset)
|
||||
}
|
||||
}
|
||||
|
||||
let currentY = surfaceView.frame.minY
|
||||
let targetPosition = self.targetPosition(from: currentY, with: velocity)
|
||||
let distance = self.distance(to: targetPosition)
|
||||
|
||||
endInteraction(for: targetPosition)
|
||||
|
||||
if isRemovalInteractionEnabled, isBottomState {
|
||||
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: min(velocity.y/distance, behavior.removalVelocity)) : .zero
|
||||
// `velocityVector` will be replaced by just a velocity(not vector) when FloatingPanelRemovalInteraction will be added.
|
||||
if shouldStartRemovalAnimation(with: velocityVector), let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidEndDraggingToRemove(vc, withVelocity: velocity)
|
||||
let animationVector = CGVector(dx: abs(velocityVector.dx), dy: abs(velocityVector.dy))
|
||||
startRemovalAnimation(vc, with: animationVector) { [weak self] in
|
||||
self?.finishRemovalAnimation()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if scrollView != nil, !stopScrollDeceleration,
|
||||
surfaceView.frame.minY == layoutAdapter.topY,
|
||||
targetPosition == layoutAdapter.topMostState {
|
||||
self.state = targetPosition
|
||||
self.updateLayout(to: targetPosition)
|
||||
self.unlockScrollView()
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidEndDragging(vc, withVelocity: .zero, targetPosition: targetPosition)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidEndDragging(vc, withVelocity: velocity, targetPosition: targetPosition)
|
||||
}
|
||||
|
||||
// Workaround: Disable a tracking scroll to prevent bouncing a scroll content in a panel animating
|
||||
let isScrollEnabled = scrollView?.isScrollEnabled
|
||||
if let scrollView = scrollView, targetPosition != .full {
|
||||
scrollView.isScrollEnabled = false
|
||||
}
|
||||
|
||||
startAnimation(to: targetPosition, at: distance, with: velocity)
|
||||
|
||||
// Workaround: Reset `self.scrollView.isScrollEnabled`
|
||||
if let scrollView = scrollView, targetPosition != .full,
|
||||
let isScrollEnabled = isScrollEnabled {
|
||||
scrollView.isScrollEnabled = isScrollEnabled
|
||||
}
|
||||
}
|
||||
|
||||
private func shouldStartRemovalAnimation(with velocityVector: CGVector) -> Bool {
|
||||
let posY = layoutAdapter.positionY(for: state)
|
||||
let currentY = surfaceView.frame.minY
|
||||
let hiddenY = layoutAdapter.positionY(for: .hidden)
|
||||
let vth = behavior.removalVelocity
|
||||
let pth = max(min(behavior.removalProgress, 1.0), 0.0)
|
||||
|
||||
let num = (currentY - posY)
|
||||
let den = (hiddenY - posY)
|
||||
|
||||
guard num >= 0, den != 0, (num / den >= pth || velocityVector.dy == vth)
|
||||
else { return false }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private func startRemovalAnimation(_ vc: FloatingPanelController, with velocityVector: CGVector, completion: (() -> Void)?) {
|
||||
let animator = behavior.removalInteractionAnimator(vc, with: velocityVector)
|
||||
|
||||
animator.addAnimations { [weak self] in
|
||||
self?.state = .hidden
|
||||
self?.updateLayout(to: .hidden)
|
||||
}
|
||||
animator.addCompletion({ _ in
|
||||
self.animator = nil
|
||||
completion?()
|
||||
})
|
||||
self.animator = animator
|
||||
animator.startAnimation()
|
||||
}
|
||||
|
||||
private func finishRemovalAnimation() {
|
||||
viewcontroller?.dismiss(animated: false) { [weak self] in
|
||||
guard let vc = self?.viewcontroller else { return }
|
||||
vc.delegate?.floatingPanelDidEndRemove(vc)
|
||||
}
|
||||
}
|
||||
|
||||
private func startInteraction(with translation: CGPoint, at location: CGPoint) {
|
||||
/* Don't lock a scroll view to show a scroll indicator after hitting the top */
|
||||
log.debug("startInteraction -- translation = \(translation.y), location = \(location.y)")
|
||||
guard interactionInProgress == false else { return }
|
||||
|
||||
var offset: CGPoint = .zero
|
||||
|
||||
initialFrame = surfaceView.frame
|
||||
if state == layoutAdapter.topMostState, let scrollView = scrollView {
|
||||
if grabberAreaFrame.contains(location) || scrollView.isTracking == false {
|
||||
initialScrollOffset = scrollView.contentOffset
|
||||
} else {
|
||||
initialScrollOffset = scrollView.contentOffsetZero
|
||||
// Fit the surface bounds to a scroll offset content by startInteraction(at:offset:)
|
||||
let scrollOffsetY = (scrollView.contentOffset.y - scrollView.contentOffsetZero.y)
|
||||
if scrollOffsetY < 0 {
|
||||
offset = CGPoint(x: -scrollView.contentOffset.x, y: -scrollOffsetY)
|
||||
}
|
||||
}
|
||||
log.debug("initial scroll offset --", initialScrollOffset)
|
||||
}
|
||||
|
||||
initialTranslationY = translation.y
|
||||
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelWillBeginDragging(vc)
|
||||
}
|
||||
|
||||
layoutAdapter.startInteraction(at: state, offset: offset)
|
||||
|
||||
interactionInProgress = true
|
||||
|
||||
lockScrollView()
|
||||
}
|
||||
|
||||
private func endInteraction(for targetPosition: FloatingPanelPosition) {
|
||||
log.debug("endInteraction to \(targetPosition)")
|
||||
|
||||
if let scrollView = scrollView {
|
||||
log.debug("endInteraction -- scroll offset = \(scrollView.contentOffset)")
|
||||
}
|
||||
|
||||
interactionInProgress = false
|
||||
|
||||
// Prevent to keep a scroll view indicator visible at the half/tip position
|
||||
if targetPosition != layoutAdapter.topMostState {
|
||||
lockScrollView()
|
||||
}
|
||||
|
||||
layoutAdapter.endInteraction(at: targetPosition)
|
||||
}
|
||||
|
||||
private func tearDownActiveInteraction() {
|
||||
// Cancel the pan gesture so that panningEnd(with:velocity:) is called
|
||||
panGestureRecognizer.isEnabled = false
|
||||
panGestureRecognizer.isEnabled = true
|
||||
}
|
||||
|
||||
private func startAnimation(to targetPosition: FloatingPanelPosition, at distance: CGFloat, with velocity: CGPoint) {
|
||||
log.debug("startAnimation to \(targetPosition) -- distance = \(distance), velocity = \(velocity.y)")
|
||||
guard let vc = viewcontroller else { return }
|
||||
|
||||
isDecelerating = true
|
||||
|
||||
vc.delegate?.floatingPanelWillBeginDecelerating(vc)
|
||||
|
||||
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: abs(velocity.y)/distance) : .zero
|
||||
let animator = behavior.interactionAnimator(vc, to: targetPosition, with: velocityVector)
|
||||
animator.addAnimations { [weak self] in
|
||||
guard let `self` = self, let vc = self.viewcontroller else { return }
|
||||
self.state = targetPosition
|
||||
if animator.isInterruptible {
|
||||
switch vc.contentMode {
|
||||
case .fitToBounds:
|
||||
UIView.performWithLinear(startTime: 0.0, relativeDuration: 0.75) {
|
||||
self.layoutAdapter.activateFixedLayout()
|
||||
self.surfaceView.superview!.layoutIfNeeded()
|
||||
}
|
||||
case .static:
|
||||
self.layoutAdapter.activateFixedLayout()
|
||||
}
|
||||
} else {
|
||||
self.layoutAdapter.activateFixedLayout()
|
||||
}
|
||||
self.layoutAdapter.activateInteractiveLayout(of: targetPosition)
|
||||
}
|
||||
animator.addCompletion { [weak self] pos in
|
||||
// Prevent calling `finishAnimation(at:)` by the old animator whose `isInterruptive` is false
|
||||
// when a new animator has been started after the old one is interrupted.
|
||||
guard let `self` = self, self.animator == animator else { return }
|
||||
log.debug("finishAnimation to \(targetPosition)")
|
||||
self.endAnimation(pos == .end)
|
||||
}
|
||||
self.animator = animator
|
||||
animator.startAnimation()
|
||||
}
|
||||
|
||||
private func endAnimation(_ finished: Bool) {
|
||||
self.isDecelerating = false
|
||||
self.animator = nil
|
||||
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidEndDecelerating(vc)
|
||||
}
|
||||
|
||||
if let scrollView = scrollView {
|
||||
log.debug("finishAnimation -- scroll offset = \(scrollView.contentOffset)")
|
||||
}
|
||||
|
||||
stopScrollDeceleration = false
|
||||
|
||||
log.debug("finishAnimation -- state = \(state) surface.minY = \(surfaceView.presentationFrame.minY) topY = \(layoutAdapter.topY)")
|
||||
if finished, state == layoutAdapter.topMostState, abs(surfaceView.presentationFrame.minY - layoutAdapter.topY) <= 1.0 {
|
||||
unlockScrollView()
|
||||
}
|
||||
}
|
||||
|
||||
private func distance(to targetPosition: FloatingPanelPosition) -> CGFloat {
|
||||
let currentY = surfaceView.frame.minY
|
||||
let targetY = layoutAdapter.positionY(for: targetPosition)
|
||||
return CGFloat(abs(currentY - targetY))
|
||||
}
|
||||
|
||||
// Distance travelled after decelerating to zero velocity at a constant rate.
|
||||
// Refer to the slides p176 of [Designing Fluid Interfaces](https://developer.apple.com/videos/play/wwdc2018/803/)
|
||||
private func project(initialVelocity: CGFloat, decelerationRate: CGFloat) -> CGFloat {
|
||||
return (initialVelocity / 1000.0) * decelerationRate / (1.0 - decelerationRate)
|
||||
}
|
||||
|
||||
func targetPosition(from currentY: CGFloat, with velocity: CGPoint) -> (FloatingPanelPosition) {
|
||||
guard let vc = viewcontroller else { return state }
|
||||
let supportedPositions = layoutAdapter.supportedPositions
|
||||
|
||||
guard supportedPositions.count > 1 else {
|
||||
return state
|
||||
}
|
||||
|
||||
let sortedPositions = Array(supportedPositions).sorted(by: { $0.rawValue < $1.rawValue })
|
||||
|
||||
// Projection
|
||||
let decelerationRate = behavior.momentumProjectionRate(vc)
|
||||
let baseY = abs(layoutAdapter.positionY(for: layoutAdapter.bottomMostState) - layoutAdapter.positionY(for: layoutAdapter.topMostState))
|
||||
let vecY = velocity.y / baseY
|
||||
var pY = project(initialVelocity: vecY, decelerationRate: decelerationRate) * baseY + currentY
|
||||
|
||||
let forwardY = velocity.y == 0 ? (currentY - layoutAdapter.positionY(for: state) > 0) : velocity.y > 0
|
||||
|
||||
let segment = layoutAdapter.segument(at: pY, forward: forwardY)
|
||||
|
||||
var fromPos: FloatingPanelPosition
|
||||
var toPos: FloatingPanelPosition
|
||||
|
||||
let (lowerPos, upperPos) = (segment.lower ?? sortedPositions.first!, segment.upper ?? sortedPositions.last!)
|
||||
(fromPos, toPos) = forwardY ? (lowerPos, upperPos) : (upperPos, lowerPos)
|
||||
|
||||
if behavior.shouldProjectMomentum(vc, for: toPos) == false {
|
||||
let segment = layoutAdapter.segument(at: currentY, forward: forwardY)
|
||||
var (lowerPos, upperPos) = (segment.lower ?? sortedPositions.first!, segment.upper ?? sortedPositions.last!)
|
||||
// Equate the segment out of {top,bottom} most state to the {top,bottom} most segment
|
||||
if lowerPos == upperPos {
|
||||
if forwardY {
|
||||
upperPos = lowerPos.next(in: sortedPositions)
|
||||
} else {
|
||||
lowerPos = upperPos.pre(in: sortedPositions)
|
||||
}
|
||||
}
|
||||
(fromPos, toPos) = forwardY ? (lowerPos, upperPos) : (upperPos, lowerPos)
|
||||
// Block a projection to a segment over the next from the current segment
|
||||
// (= Trim pY with the current segment)
|
||||
if forwardY {
|
||||
pY = max(min(pY, layoutAdapter.positionY(for: toPos.next(in: sortedPositions))), layoutAdapter.positionY(for: fromPos))
|
||||
} else {
|
||||
pY = max(min(pY, layoutAdapter.positionY(for: fromPos)), layoutAdapter.positionY(for: toPos.pre(in: sortedPositions)))
|
||||
}
|
||||
}
|
||||
|
||||
// Redirection
|
||||
let redirectionalProgress = max(min(behavior.redirectionalProgress(vc, from: fromPos, to: toPos), 1.0), 0.0)
|
||||
let progress = abs(pY - layoutAdapter.positionY(for: fromPos)) / abs(layoutAdapter.positionY(for: fromPos) - layoutAdapter.positionY(for: toPos))
|
||||
return progress > redirectionalProgress ? toPos : fromPos
|
||||
}
|
||||
|
||||
// MARK: - ScrollView handling
|
||||
|
||||
private func lockScrollView() {
|
||||
guard let scrollView = scrollView else { return }
|
||||
|
||||
if scrollView.isLocked {
|
||||
log.debug("Already scroll locked.")
|
||||
return
|
||||
}
|
||||
log.debug("lock scroll view")
|
||||
|
||||
scrollBouncable = scrollView.bounces
|
||||
scrollIndictorVisible = scrollView.showsVerticalScrollIndicator
|
||||
|
||||
scrollView.isDirectionalLockEnabled = true
|
||||
scrollView.bounces = false
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
}
|
||||
|
||||
private func unlockScrollView() {
|
||||
guard let scrollView = scrollView, scrollView.isLocked else { return }
|
||||
log.debug("unlock scroll view")
|
||||
|
||||
scrollView.isDirectionalLockEnabled = false
|
||||
scrollView.bounces = scrollBouncable
|
||||
scrollView.showsVerticalScrollIndicator = scrollIndictorVisible
|
||||
}
|
||||
|
||||
private func stopScrollingWithDeceleration(at contentOffset: CGPoint) {
|
||||
// Must use setContentOffset(_:animated) to force-stop deceleration
|
||||
scrollView?.setContentOffset(contentOffset, animated: false)
|
||||
}
|
||||
|
||||
private func allowScrollPanGesture(at contentOffset: CGPoint) -> Bool {
|
||||
if state == layoutAdapter.topMostState {
|
||||
return contentOffset.y <= -30.0 || contentOffset.y > 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
|
||||
fileprivate weak var floatingPanel: FloatingPanelCore?
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
if floatingPanel?.animator != nil {
|
||||
self.state = .began
|
||||
}
|
||||
}
|
||||
override weak var delegate: UIGestureRecognizerDelegate? {
|
||||
get {
|
||||
return super.delegate
|
||||
}
|
||||
set {
|
||||
guard newValue is FloatingPanelCore else {
|
||||
let exception = NSException(name: .invalidArgumentException,
|
||||
reason: "FloatingPanelController's built-in pan gesture recognizer must have its controller as its delegate.",
|
||||
userInfo: nil)
|
||||
exception.raise()
|
||||
return
|
||||
}
|
||||
super.delegate = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,128 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/09/27.
|
||||
// Copyright © 2018 scenee. All rights reserved.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
public protocol FloatingPanelLayout: class {
|
||||
/// Returns the initial position of a floating panel
|
||||
var initialPosition: FloatingPanelPosition { get }
|
||||
/// Returns an array of FloatingPanelPosition object to tell the applicable position the floating panel controller
|
||||
var supportedPositions: [FloatingPanelPosition] { get }
|
||||
/// FloatingPanelFullScreenLayout
|
||||
///
|
||||
/// Use the layout protocol if you configure full, half and tip insets from the superview, not the safe area.
|
||||
/// It can't be used with FloatingPanelIntrinsicLayout.
|
||||
public protocol FloatingPanelFullScreenLayout: FloatingPanelLayout { }
|
||||
|
||||
/// Return the interaction buffer of full position. Default is 6.0.
|
||||
public extension FloatingPanelFullScreenLayout {
|
||||
var positionReference: FloatingPanelLayoutReference {
|
||||
return .fromSuperview
|
||||
}
|
||||
}
|
||||
|
||||
/// FloatingPanelIntrinsicLayout
|
||||
///
|
||||
/// Use the layout protocol if you want to layout a panel using the intrinsic height.
|
||||
/// It can't be used with `FloatingPanelFullScreenLayout`.
|
||||
///
|
||||
/// - Attention:
|
||||
/// `insetFor(position:)` must return `nil` for the full position. Because
|
||||
/// the inset is determined automatically by the intrinsic height.
|
||||
/// You can customize insets only for the half, tip and hidden positions.
|
||||
///
|
||||
/// - Note:
|
||||
/// By default, the `positionReference` is set to `.fromSafeArea`.
|
||||
public protocol FloatingPanelIntrinsicLayout: FloatingPanelLayout { }
|
||||
|
||||
public extension FloatingPanelIntrinsicLayout {
|
||||
var initialPosition: FloatingPanelPosition {
|
||||
return .full
|
||||
}
|
||||
|
||||
var supportedPositions: Set<FloatingPanelPosition> {
|
||||
return [.full]
|
||||
}
|
||||
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
|
||||
return nil
|
||||
}
|
||||
|
||||
var positionReference: FloatingPanelLayoutReference {
|
||||
return .fromSafeArea
|
||||
}
|
||||
}
|
||||
|
||||
public enum FloatingPanelLayoutReference: Int {
|
||||
case fromSafeArea = 0
|
||||
case fromSuperview = 1
|
||||
}
|
||||
|
||||
public protocol FloatingPanelLayout: class {
|
||||
/// Returns the initial position of a floating panel.
|
||||
var initialPosition: FloatingPanelPosition { get }
|
||||
|
||||
/// Returns a set of FloatingPanelPosition objects to tell the applicable
|
||||
/// positions of the floating panel controller.
|
||||
///
|
||||
/// By default, it returns full, half and tip positions.
|
||||
var supportedPositions: Set<FloatingPanelPosition> { get }
|
||||
|
||||
/// Return the interaction buffer to the top from the top position. Default is 6.0.
|
||||
var topInteractionBuffer: CGFloat { get }
|
||||
/// Return the interaction buffer of full position. Default is 6.0.
|
||||
|
||||
/// Return the interaction buffer to the bottom from the bottom position. Default is 6.0.
|
||||
///
|
||||
/// - Important:
|
||||
/// The specified buffer is ignored when `FloatingPanelController.isRemovalInteractionEnabled` is set to true.
|
||||
var bottomInteractionBuffer: CGFloat { get }
|
||||
|
||||
/// Returns a CGFloat value for a floating panel position(full, half, tip).
|
||||
/// A value for full position indicates an inset from the safe area top.
|
||||
/// On the other hand, values fro half and tip positions indicate insets from the safe area bottom.
|
||||
/// If a position doesn't contain the supported positions, return nil.
|
||||
/// Returns a CGFloat value to determine a Y coordinate of a floating panel for each position(full, half, tip and hidden).
|
||||
///
|
||||
/// Its returning value indicates a different inset for each position.
|
||||
/// For full position, a top inset from a safe area in `FloatingPanelController.view`.
|
||||
/// For half or tip position, a bottom inset from the safe area.
|
||||
/// For hidden position, a bottom inset from `FloatingPanelController.view`.
|
||||
/// If a position isn't supported or the default value is used, return nil.
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat?
|
||||
/// Returns layout constraints for a surface view of a floaitng panel.
|
||||
/// The layout constraints must not include ones for topAnchor and bottomAnchor
|
||||
/// because constarints for them will be added by the floating panel controller.
|
||||
|
||||
/// Returns X-axis and width layout constraints of the surface view of a floating panel.
|
||||
/// You must not include any Y-axis and height layout constraints of the surface view
|
||||
/// because their constraints will be configured by the floating panel controller.
|
||||
/// By default, the width of a surface view fits a safe area.
|
||||
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint]
|
||||
|
||||
/// Return the backdrop alpha of black color in full position. Default is 0.3.
|
||||
var backdropAlpha: CGFloat { get }
|
||||
/// Returns a CGFloat value to determine the backdrop view's alpha for a position.
|
||||
///
|
||||
/// Default is 0.3 at full position, otherwise 0.0.
|
||||
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat
|
||||
|
||||
var positionReference: FloatingPanelLayoutReference { get }
|
||||
|
||||
}
|
||||
|
||||
public extension FloatingPanelLayout {
|
||||
var backdropAlpha: CGFloat { return 0.3 }
|
||||
var topInteractionBuffer: CGFloat { return 6.0 }
|
||||
var bottomInteractionBuffer: CGFloat { return 6.0 }
|
||||
|
||||
var supportedPositions: Set<FloatingPanelPosition> {
|
||||
return Set([.full, .half, .tip])
|
||||
}
|
||||
|
||||
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
|
||||
return [
|
||||
surfaceView.leftAnchor.constraint(equalTo: view.sideLayoutGuide.leftAnchor, constant: 0.0),
|
||||
surfaceView.rightAnchor.constraint(equalTo: view.sideLayoutGuide.rightAnchor, constant: 0.0),
|
||||
]
|
||||
}
|
||||
|
||||
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
|
||||
return position == .full ? 0.3 : 0.0
|
||||
}
|
||||
|
||||
var positionReference: FloatingPanelLayoutReference {
|
||||
return .fromSafeArea
|
||||
}
|
||||
}
|
||||
|
||||
public class FloatingPanelDefaultLayout: FloatingPanelLayout {
|
||||
public var supportedPositions: [FloatingPanelPosition] {
|
||||
return [.full, .half, .tip]
|
||||
}
|
||||
public init() { }
|
||||
|
||||
public var initialPosition: FloatingPanelPosition {
|
||||
return .half
|
||||
@@ -50,22 +133,18 @@ public class FloatingPanelDefaultLayout: FloatingPanelLayout {
|
||||
case .full: return 18.0
|
||||
case .half: return 262.0
|
||||
case .tip: return 69.0
|
||||
case .hidden: return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
|
||||
return [
|
||||
surfaceView.leftAnchor.constraint(equalTo: view.sideLayoutGuide.leftAnchor, constant: 0.0),
|
||||
surfaceView.rightAnchor.constraint(equalTo: view.sideLayoutGuide.rightAnchor, constant: 0.0),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout {
|
||||
public init() { }
|
||||
|
||||
public var initialPosition: FloatingPanelPosition {
|
||||
return .tip
|
||||
}
|
||||
public var supportedPositions: [FloatingPanelPosition] {
|
||||
public var supportedPositions: Set<FloatingPanelPosition> {
|
||||
return [.full, .tip]
|
||||
}
|
||||
|
||||
@@ -76,144 +155,462 @@ public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout {
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
|
||||
return [
|
||||
surfaceView.leftAnchor.constraint(equalTo: view.sideLayoutGuide.leftAnchor, constant: 8.0),
|
||||
surfaceView.widthAnchor.constraint(equalToConstant: 291),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
struct LayoutSegment {
|
||||
let lower: FloatingPanelPosition?
|
||||
let upper: FloatingPanelPosition?
|
||||
}
|
||||
|
||||
class FloatingPanelLayoutAdapter {
|
||||
weak var vc: FloatingPanelController!
|
||||
private weak var surfaceView: FloatingPanelSurfaceView!
|
||||
private weak var backdropView: FloatingPanelBackdropView!
|
||||
|
||||
var layout: FloatingPanelLayout
|
||||
var layout: FloatingPanelLayout {
|
||||
didSet {
|
||||
checkLayoutConsistance()
|
||||
}
|
||||
}
|
||||
|
||||
var safeAreaInsets: UIEdgeInsets = .zero
|
||||
private var safeAreaInsets: UIEdgeInsets {
|
||||
return vc?.layoutInsets ?? .zero
|
||||
}
|
||||
|
||||
private var initialConst: CGFloat = 0.0
|
||||
|
||||
private var heightBuffer: CGFloat = 88.0 // For bounce
|
||||
private var fixedConstraints: [NSLayoutConstraint] = []
|
||||
private var fullConstraints: [NSLayoutConstraint] = []
|
||||
private var halfConstraints: [NSLayoutConstraint] = []
|
||||
private var tipConstraints: [NSLayoutConstraint] = []
|
||||
private var offConstraints: [NSLayoutConstraint] = []
|
||||
private var heightConstraints: NSLayoutConstraint? = nil
|
||||
private var interactiveTopConstraint: NSLayoutConstraint?
|
||||
private var bottomConstraint: NSLayoutConstraint?
|
||||
|
||||
var topInset: CGFloat {
|
||||
return layout.insetFor(position: .full) ?? 0.0
|
||||
|
||||
private var heightConstraint: NSLayoutConstraint?
|
||||
|
||||
private var fullInset: CGFloat {
|
||||
if layout is FloatingPanelIntrinsicLayout {
|
||||
return intrinsicHeight
|
||||
} else {
|
||||
return layout.insetFor(position: .full) ?? 0.0
|
||||
}
|
||||
}
|
||||
var halfInset: CGFloat {
|
||||
private var halfInset: CGFloat {
|
||||
return layout.insetFor(position: .half) ?? 0.0
|
||||
}
|
||||
var tipInset: CGFloat {
|
||||
private var tipInset: CGFloat {
|
||||
return layout.insetFor(position: .tip) ?? 0.0
|
||||
}
|
||||
private var hiddenInset: CGFloat {
|
||||
return layout.insetFor(position: .hidden) ?? 0.0
|
||||
}
|
||||
|
||||
var supportedPositions: Set<FloatingPanelPosition> {
|
||||
return layout.supportedPositions
|
||||
}
|
||||
|
||||
var topMostState: FloatingPanelPosition {
|
||||
return supportedPositions.sorted(by: { $0.rawValue < $1.rawValue }).first ?? .hidden
|
||||
}
|
||||
|
||||
var bottomMostState: FloatingPanelPosition {
|
||||
return supportedPositions.sorted(by: { $0.rawValue < $1.rawValue }).last ?? .hidden
|
||||
}
|
||||
|
||||
var topY: CGFloat {
|
||||
return (safeAreaInsets.top + topInset)
|
||||
}
|
||||
|
||||
var middleY: CGFloat {
|
||||
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + halfInset)
|
||||
return positionY(for: topMostState)
|
||||
}
|
||||
|
||||
var bottomY: CGFloat {
|
||||
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
|
||||
return positionY(for: bottomMostState)
|
||||
}
|
||||
|
||||
var topMaxY: CGFloat {
|
||||
return topY - layout.topInteractionBuffer
|
||||
}
|
||||
|
||||
var bottomMaxY: CGFloat {
|
||||
return bottomY + layout.bottomInteractionBuffer
|
||||
}
|
||||
|
||||
var adjustedContentInsets: UIEdgeInsets {
|
||||
return UIEdgeInsets(top: 0.0,
|
||||
left: 0.0,
|
||||
bottom: safeAreaInsets.bottom,
|
||||
right: 0.0)
|
||||
}
|
||||
|
||||
func positionY(for pos: FloatingPanelPosition) -> CGFloat {
|
||||
switch pos {
|
||||
case .full:
|
||||
return topY
|
||||
if layout is FloatingPanelIntrinsicLayout {
|
||||
return surfaceView.superview!.bounds.height - surfaceView.bounds.height
|
||||
}
|
||||
switch layout.positionReference {
|
||||
case .fromSafeArea:
|
||||
return (safeAreaInsets.top + fullInset)
|
||||
case .fromSuperview:
|
||||
return fullInset
|
||||
}
|
||||
case .half:
|
||||
return middleY
|
||||
switch layout.positionReference {
|
||||
case .fromSafeArea:
|
||||
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + halfInset)
|
||||
case .fromSuperview:
|
||||
return surfaceView.superview!.bounds.height - halfInset
|
||||
}
|
||||
case .tip:
|
||||
return bottomY
|
||||
switch layout.positionReference {
|
||||
case .fromSafeArea:
|
||||
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
|
||||
case .fromSuperview:
|
||||
return surfaceView.superview!.bounds.height - tipInset
|
||||
}
|
||||
case .hidden:
|
||||
return surfaceView.superview!.bounds.height - hiddenInset
|
||||
}
|
||||
}
|
||||
|
||||
init(surfaceView: FloatingPanelSurfaceView, layout: FloatingPanelLayout) {
|
||||
var intrinsicHeight: CGFloat = 0.0
|
||||
|
||||
init(surfaceView: FloatingPanelSurfaceView, backdropView: FloatingPanelBackdropView, layout: FloatingPanelLayout) {
|
||||
self.layout = layout
|
||||
self.surfaceView = surfaceView
|
||||
|
||||
// Verify layout configurations
|
||||
assert(layout.supportedPositions.count > 1)
|
||||
assert(layout.supportedPositions.contains(layout.initialPosition))
|
||||
if halfInset > 0 {
|
||||
assert(halfInset >= tipInset)
|
||||
}
|
||||
self.backdropView = backdropView
|
||||
}
|
||||
|
||||
func prepareLayout(toParent parent: UIViewController) {
|
||||
surfaceView.translatesAutoresizingMaskIntoConstraints = false
|
||||
func updateIntrinsicHeight() {
|
||||
#if swift(>=4.2)
|
||||
let fittingSize = UIView.layoutFittingCompressedSize
|
||||
#else
|
||||
let fittingSize = UILayoutFittingCompressedSize
|
||||
#endif
|
||||
var intrinsicHeight = surfaceView.contentView?.systemLayoutSizeFitting(fittingSize).height ?? 0.0
|
||||
var safeAreaBottom: CGFloat = 0.0
|
||||
if #available(iOS 11.0, *) {
|
||||
safeAreaBottom = surfaceView.contentView?.safeAreaInsets.bottom ?? 0.0
|
||||
if safeAreaBottom > 0 {
|
||||
intrinsicHeight -= safeAreaInsets.bottom
|
||||
}
|
||||
}
|
||||
self.intrinsicHeight = max(intrinsicHeight, 0.0)
|
||||
|
||||
log.debug("Update intrinsic height =", intrinsicHeight,
|
||||
", surface(height) =", surfaceView.frame.height,
|
||||
", content(height) =", surfaceView.contentView?.frame.height ?? 0.0,
|
||||
", content safe area(bottom) =", safeAreaBottom)
|
||||
}
|
||||
|
||||
func prepareLayout(in vc: FloatingPanelController) {
|
||||
self.vc = vc
|
||||
|
||||
NSLayoutConstraint.deactivate(fixedConstraints + fullConstraints + halfConstraints + tipConstraints + offConstraints)
|
||||
NSLayoutConstraint.deactivate(constraint: self.heightConstraint)
|
||||
self.heightConstraint = nil
|
||||
NSLayoutConstraint.deactivate(constraint: self.bottomConstraint)
|
||||
self.bottomConstraint = nil
|
||||
|
||||
fixedConstraints = layout.prepareLayout(surfaceView: surfaceView, in: parent.view!)
|
||||
surfaceView.translatesAutoresizingMaskIntoConstraints = false
|
||||
backdropView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// Fixed constraints of surface and backdrop views
|
||||
let surfaceConstraints = layout.prepareLayout(surfaceView: surfaceView, in: vc.view!)
|
||||
let backdropConstraints = [
|
||||
backdropView.topAnchor.constraint(equalTo: vc.view.topAnchor, constant: 0.0),
|
||||
backdropView.leftAnchor.constraint(equalTo: vc.view.leftAnchor,constant: 0.0),
|
||||
backdropView.rightAnchor.constraint(equalTo: vc.view.rightAnchor, constant: 0.0),
|
||||
backdropView.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor, constant: 0.0),
|
||||
]
|
||||
|
||||
fixedConstraints = surfaceConstraints + backdropConstraints
|
||||
|
||||
if vc.contentMode == .fitToBounds {
|
||||
bottomConstraint = surfaceView.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor,
|
||||
constant: 0.0)
|
||||
}
|
||||
|
||||
// Flexible surface constraints for full, half, tip and off
|
||||
let topAnchor: NSLayoutYAxisAnchor = {
|
||||
if layout.positionReference == .fromSuperview {
|
||||
return vc.view.topAnchor
|
||||
} else {
|
||||
return vc.layoutGuide.topAnchor
|
||||
}
|
||||
}()
|
||||
|
||||
switch layout {
|
||||
case is FloatingPanelIntrinsicLayout:
|
||||
// Set up on updateHeight()
|
||||
break
|
||||
default:
|
||||
fullConstraints = [
|
||||
surfaceView.topAnchor.constraint(equalTo: topAnchor,
|
||||
constant: fullInset),
|
||||
]
|
||||
}
|
||||
|
||||
let bottomAnchor: NSLayoutYAxisAnchor = {
|
||||
if layout.positionReference == .fromSuperview {
|
||||
return vc.view.bottomAnchor
|
||||
} else {
|
||||
return vc.layoutGuide.bottomAnchor
|
||||
}
|
||||
}()
|
||||
|
||||
fullConstraints = [
|
||||
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.topAnchor,
|
||||
constant: topInset),
|
||||
]
|
||||
halfConstraints = [
|
||||
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.bottomAnchor,
|
||||
surfaceView.topAnchor.constraint(equalTo: bottomAnchor,
|
||||
constant: -halfInset),
|
||||
]
|
||||
tipConstraints = [
|
||||
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.bottomAnchor,
|
||||
surfaceView.topAnchor.constraint(equalTo: bottomAnchor,
|
||||
constant: -tipInset),
|
||||
]
|
||||
|
||||
offConstraints = [
|
||||
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.bottomAnchor, constant: 0.0),
|
||||
surfaceView.topAnchor.constraint(equalTo:vc.view.bottomAnchor,
|
||||
constant: -hiddenInset),
|
||||
]
|
||||
}
|
||||
|
||||
func startInteraction(at state: FloatingPanelPosition, offset: CGPoint = .zero) {
|
||||
guard self.interactiveTopConstraint == nil else { return }
|
||||
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
|
||||
|
||||
let interactiveTopConstraint: NSLayoutConstraint
|
||||
switch layout.positionReference {
|
||||
case .fromSafeArea:
|
||||
initialConst = surfaceView.frame.minY - safeAreaInsets.top + offset.y
|
||||
interactiveTopConstraint = surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.topAnchor,
|
||||
constant: initialConst)
|
||||
case .fromSuperview:
|
||||
initialConst = surfaceView.frame.minY + offset.y
|
||||
interactiveTopConstraint = surfaceView.topAnchor.constraint(equalTo: vc.view.topAnchor,
|
||||
constant: initialConst)
|
||||
}
|
||||
NSLayoutConstraint.activate([interactiveTopConstraint])
|
||||
self.interactiveTopConstraint = interactiveTopConstraint
|
||||
}
|
||||
|
||||
func endInteraction(at state: FloatingPanelPosition) {
|
||||
// Don't deactivate `interactiveTopConstraint` here because it leads to
|
||||
// unsatisfiable constraints
|
||||
|
||||
if self.interactiveTopConstraint == nil {
|
||||
// Actiavate `interactiveTopConstraint` for `fitToBounds` mode.
|
||||
// It goes throught this path when the pan gesture state jumps
|
||||
// from .begin to .end.
|
||||
startInteraction(at: state)
|
||||
}
|
||||
}
|
||||
|
||||
// The method is separated from prepareLayout(to:) for the rotation support
|
||||
// It must be called in FloatingPanelController.traitCollectionDidChange(_:)
|
||||
func updateHeight() {
|
||||
guard let vc = vc else { return }
|
||||
NSLayoutConstraint.deactivate(constraint: heightConstraint)
|
||||
heightConstraint = nil
|
||||
|
||||
if layout is FloatingPanelIntrinsicLayout {
|
||||
updateIntrinsicHeight()
|
||||
}
|
||||
defer {
|
||||
UIView.performWithoutAnimation {
|
||||
surfaceView.superview!.layoutIfNeeded()
|
||||
if layout is FloatingPanelIntrinsicLayout {
|
||||
NSLayoutConstraint.deactivate(fullConstraints)
|
||||
fullConstraints = [
|
||||
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
|
||||
constant: -fullInset),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
if let heightConstraints = self.heightConstraints {
|
||||
NSLayoutConstraint.deactivate([heightConstraints])
|
||||
guard vc.contentMode != .fitToBounds else { return }
|
||||
|
||||
switch layout {
|
||||
case is FloatingPanelIntrinsicLayout:
|
||||
heightConstraint = surfaceView.heightAnchor.constraint(equalToConstant: intrinsicHeight + safeAreaInsets.bottom)
|
||||
default:
|
||||
let const = -(positionY(for: topMostState))
|
||||
heightConstraint = surfaceView.heightAnchor.constraint(equalTo: vc.view.heightAnchor,
|
||||
constant: const)
|
||||
}
|
||||
let heightConstraints = surfaceView.heightAnchor.constraint(equalToConstant: UIScreen.main.bounds.height + heightBuffer)
|
||||
NSLayoutConstraint.activate([heightConstraints])
|
||||
self.heightConstraints = heightConstraints
|
||||
NSLayoutConstraint.activate(constraint: heightConstraint)
|
||||
|
||||
surfaceView.bottomOverflow = vc.view.bounds.height + layout.topInteractionBuffer
|
||||
}
|
||||
|
||||
func activateLayout(of state: FloatingPanelPosition?) {
|
||||
func updateInteractiveTopConstraint(diff: CGFloat, allowsTopBuffer: Bool, with behavior: FloatingPanelBehavior) {
|
||||
defer {
|
||||
surfaceView.superview!.layoutIfNeeded()
|
||||
layoutSurfaceIfNeeded() // MUST be called to update `surfaceView.frame`
|
||||
}
|
||||
|
||||
let topMostConst: CGFloat = {
|
||||
var ret: CGFloat = 0.0
|
||||
switch layout.positionReference {
|
||||
case .fromSafeArea:
|
||||
ret = topY - safeAreaInsets.top
|
||||
case .fromSuperview:
|
||||
ret = topY
|
||||
}
|
||||
return max(ret, 0.0) // The top boundary is equal to the related topAnchor.
|
||||
}()
|
||||
let bottomMostConst: CGFloat = {
|
||||
var ret: CGFloat = 0.0
|
||||
let _bottomY = vc.isRemovalInteractionEnabled ? positionY(for: .hidden) : bottomY
|
||||
switch layout.positionReference {
|
||||
case .fromSafeArea:
|
||||
ret = _bottomY - safeAreaInsets.top
|
||||
case .fromSuperview:
|
||||
ret = _bottomY
|
||||
}
|
||||
return min(ret, surfaceView.superview!.bounds.height)
|
||||
}()
|
||||
let minConst = allowsTopBuffer ? topMostConst - layout.topInteractionBuffer : topMostConst
|
||||
let maxConst = bottomMostConst + layout.bottomInteractionBuffer
|
||||
|
||||
var const = initialConst + diff
|
||||
|
||||
// Rubberbanding top buffer
|
||||
if behavior.allowsRubberBanding(for: .top), const < topMostConst {
|
||||
let buffer = topMostConst - const
|
||||
const = topMostConst - rubberbandEffect(for: buffer, base: vc.view.bounds.height)
|
||||
}
|
||||
|
||||
// Rubberbanding bottom buffer
|
||||
if behavior.allowsRubberBanding(for: .bottom), const > bottomMostConst {
|
||||
let buffer = const - bottomMostConst
|
||||
const = bottomMostConst + rubberbandEffect(for: buffer, base: vc.view.bounds.height)
|
||||
}
|
||||
|
||||
interactiveTopConstraint?.constant = max(minConst, min(maxConst, const))
|
||||
}
|
||||
|
||||
// According to @chpwn's tweet: https://twitter.com/chpwn/status/285540192096497664
|
||||
// x = distance from the edge
|
||||
// c = constant value, UIScrollView uses 0.55
|
||||
// d = dimension, either width or height
|
||||
private func rubberbandEffect(for buffer: CGFloat, base: CGFloat) -> CGFloat {
|
||||
return (1.0 - (1.0 / ((buffer * 0.55 / base) + 1.0))) * base
|
||||
}
|
||||
|
||||
func activateFixedLayout() {
|
||||
// Must deactivate `interactiveTopConstraint` here
|
||||
NSLayoutConstraint.deactivate(constraint: self.interactiveTopConstraint)
|
||||
self.interactiveTopConstraint = nil
|
||||
|
||||
NSLayoutConstraint.activate(fixedConstraints)
|
||||
|
||||
guard var state = state else {
|
||||
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints)
|
||||
NSLayoutConstraint.activate(offConstraints)
|
||||
return
|
||||
if vc.contentMode == .fitToBounds {
|
||||
NSLayoutConstraint.activate(constraint: self.bottomConstraint)
|
||||
}
|
||||
}
|
||||
|
||||
func activateInteractiveLayout(of state: FloatingPanelPosition) {
|
||||
defer {
|
||||
layoutSurfaceIfNeeded()
|
||||
log.debug("activateLayout -- surface.presentation = \(self.surfaceView.presentationFrame) surface.frame = \(self.surfaceView.frame)")
|
||||
}
|
||||
|
||||
if layout.supportedPositions.contains(state) == false {
|
||||
var state = state
|
||||
|
||||
setBackdropAlpha(of: state)
|
||||
|
||||
if isValid(state) == false {
|
||||
state = layout.initialPosition
|
||||
}
|
||||
|
||||
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
|
||||
switch state {
|
||||
case .full:
|
||||
NSLayoutConstraint.deactivate(halfConstraints + tipConstraints + offConstraints)
|
||||
NSLayoutConstraint.activate(fullConstraints)
|
||||
case .half:
|
||||
NSLayoutConstraint.deactivate(fullConstraints + tipConstraints + offConstraints)
|
||||
NSLayoutConstraint.activate(halfConstraints)
|
||||
case .tip:
|
||||
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + offConstraints)
|
||||
NSLayoutConstraint.activate(tipConstraints)
|
||||
case .hidden:
|
||||
NSLayoutConstraint.activate(offConstraints)
|
||||
}
|
||||
}
|
||||
|
||||
func activateLayout(of state: FloatingPanelPosition) {
|
||||
activateFixedLayout()
|
||||
activateInteractiveLayout(of: state)
|
||||
}
|
||||
|
||||
func isValid(_ state: FloatingPanelPosition) -> Bool {
|
||||
return supportedPositions.union([.hidden]).contains(state)
|
||||
}
|
||||
|
||||
private func layoutSurfaceIfNeeded() {
|
||||
#if !TEST
|
||||
guard surfaceView.window != nil else { return }
|
||||
#endif
|
||||
surfaceView.superview?.layoutIfNeeded()
|
||||
}
|
||||
|
||||
private func setBackdropAlpha(of target: FloatingPanelPosition) {
|
||||
if target == .hidden {
|
||||
self.backdropView.alpha = 0.0
|
||||
} else {
|
||||
self.backdropView.alpha = layout.backdropAlphaFor(position: target)
|
||||
}
|
||||
}
|
||||
|
||||
private func checkLayoutConsistance() {
|
||||
// Verify layout configurations
|
||||
assert(supportedPositions.count > 0)
|
||||
assert(supportedPositions.contains(layout.initialPosition),
|
||||
"Does not include an initial position (\(layout.initialPosition)) in supportedPositions (\(supportedPositions))")
|
||||
|
||||
if layout is FloatingPanelIntrinsicLayout {
|
||||
assert(layout.insetFor(position: .full) == nil, "Return `nil` for full position on FloatingPanelIntrinsicLayout")
|
||||
}
|
||||
|
||||
if halfInset > 0 {
|
||||
assert(halfInset > tipInset, "Invalid half and tip insets")
|
||||
}
|
||||
// The verification isn't working on orientation change(portrait -> landscape)
|
||||
// of a floating panel in tab bar. Because the `safeAreaInsets.bottom` is
|
||||
// updated in delay so that it can be 83.0(not 53.0) even after the surface
|
||||
// and the super view's frame is fit to landscape already.
|
||||
/*if fullInset > 0 {
|
||||
assert(middleY > topY, "Invalid insets { topY: \(topY), middleY: \(middleY) }")
|
||||
assert(bottomY > topY, "Invalid insets { topY: \(topY), bottomY: \(bottomY) }")
|
||||
}*/
|
||||
}
|
||||
|
||||
func segument(at posY: CGFloat, forward: Bool) -> LayoutSegment {
|
||||
/// ----------------------->Y
|
||||
/// --> forward <-- backward
|
||||
/// |-------|===o===|-------| |-------|-------|===o===|
|
||||
/// |-------|-------x=======| |-------|=======x-------|
|
||||
/// |-------|-------|===o===| |-------|===o===|-------|
|
||||
/// pos: o/x, seguement: =
|
||||
let sortedPositions = supportedPositions.sorted(by: { $0.rawValue < $1.rawValue })
|
||||
|
||||
let upperIndex: Int?
|
||||
if forward {
|
||||
#if swift(>=4.2)
|
||||
upperIndex = sortedPositions.firstIndex(where: { posY < positionY(for: $0) })
|
||||
#else
|
||||
upperIndex = sortedPositions.index(where: { posY < positionY(for: $0) })
|
||||
#endif
|
||||
} else {
|
||||
#if swift(>=4.2)
|
||||
upperIndex = sortedPositions.firstIndex(where: { posY <= positionY(for: $0) })
|
||||
#else
|
||||
upperIndex = sortedPositions.index(where: { posY <= positionY(for: $0) })
|
||||
#endif
|
||||
}
|
||||
|
||||
switch upperIndex {
|
||||
case 0:
|
||||
return LayoutSegment(lower: nil, upper: sortedPositions.first)
|
||||
case let upperIndex?:
|
||||
return LayoutSegment(lower: sortedPositions[upperIndex - 1], upper: sortedPositions[upperIndex])
|
||||
default:
|
||||
return LayoutSegment(lower: sortedPositions[sortedPositions.endIndex - 1], upper: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,64 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/09/26.
|
||||
// Copyright © 2018 scenee. All rights reserved.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class FloatingPanelSurfaceContentView: UIView {}
|
||||
|
||||
/// A view that presents a surface interface in a floating panel.
|
||||
public class FloatingPanelSurfaceView: UIView {
|
||||
|
||||
/// A GrabberHandleView object displayed at the top of the surface view
|
||||
public var grabberHandle: GrabberHandleView!
|
||||
/// A GrabberHandleView object displayed at the top of the surface view.
|
||||
///
|
||||
/// To use a custom grabber handle, hide this and then add the custom one
|
||||
/// to the surface view at appropriate coordinates.
|
||||
public let grabberHandle: GrabberHandleView = GrabberHandleView()
|
||||
|
||||
/// A UIView object that can have the surface view added to it.
|
||||
public var contentView: UIView!
|
||||
/// Offset of the grabber handle from the top
|
||||
public var grabberTopPadding: CGFloat = 6.0 { didSet {
|
||||
setNeedsUpdateConstraints()
|
||||
} }
|
||||
|
||||
private var color: UIColor? = .white { didSet { setNeedsDisplay() } }
|
||||
/// The height of the grabber bar area
|
||||
public var topGrabberBarHeight: CGFloat {
|
||||
return grabberTopPadding * 2 + grabberHandleHeight
|
||||
}
|
||||
|
||||
public override var backgroundColor: UIColor? {
|
||||
get { return color }
|
||||
set {
|
||||
color = newValue
|
||||
setNeedsDisplay()
|
||||
/// Grabber view width and height
|
||||
public var grabberHandleWidth: CGFloat = 36.0 { didSet {
|
||||
setNeedsUpdateConstraints()
|
||||
} }
|
||||
public var grabberHandleHeight: CGFloat = 5.0 { didSet {
|
||||
setNeedsUpdateConstraints()
|
||||
} }
|
||||
|
||||
/// A root view of a content view controller
|
||||
public weak var contentView: UIView!
|
||||
|
||||
/// The content insets specifying the insets around the content view.
|
||||
public var contentInsets: UIEdgeInsets = .zero {
|
||||
didSet {
|
||||
// Needs update constraints
|
||||
self.setNeedsUpdateConstraints()
|
||||
}
|
||||
}
|
||||
|
||||
/// The radius to use when drawing rounded corners
|
||||
public var cornerRadius: CGFloat = 0.0 { didSet { setNeedsLayout() } }
|
||||
private var color: UIColor? = .white { didSet { setNeedsLayout() } }
|
||||
var bottomOverflow: CGFloat = 0.0 // Must not call setNeedsLayout()
|
||||
|
||||
public override var backgroundColor: UIColor? {
|
||||
get { return color }
|
||||
set { color = newValue }
|
||||
}
|
||||
|
||||
/// The radius to use when drawing top rounded corners.
|
||||
///
|
||||
/// `self.contentView` is masked with the top rounded corners automatically on iOS 11 and later.
|
||||
/// On iOS 10, they are not automatically masked because of a UIVisualEffectView issue. See https://forums.developer.apple.com/thread/50854
|
||||
public var cornerRadius: CGFloat {
|
||||
set { containerView.layer.cornerRadius = newValue; setNeedsLayout() }
|
||||
get { return containerView.layer.cornerRadius }
|
||||
}
|
||||
|
||||
/// A Boolean indicating whether the surface shadow is displayed.
|
||||
public var shadowHidden: Bool = false { didSet { setNeedsLayout() } }
|
||||
@@ -50,86 +81,162 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
/// The color of the surface border.
|
||||
public var borderWidth: CGFloat = 0.0 { didSet { setNeedsLayout() } }
|
||||
|
||||
/// The margins to use when laying out the container view wrapping content.
|
||||
public var containerMargins: UIEdgeInsets = .zero { didSet {
|
||||
setNeedsUpdateConstraints()
|
||||
} }
|
||||
|
||||
private var shadowLayer: CAShapeLayer! { didSet { setNeedsLayout() } }
|
||||
public struct Default {
|
||||
public static let grabberTopPadding: CGFloat = 6.0
|
||||
}
|
||||
private var topGrabberBarHeight: CGFloat {
|
||||
return Default.grabberTopPadding * 2 + GrabberHandleView.Default.height
|
||||
}
|
||||
/// The view presents an actual surface shape.
|
||||
///
|
||||
/// It renders the background color, border line and top rounded corners,
|
||||
/// specified by other properties. The reason why they're not be applied to
|
||||
/// a content view directly is because it avoids any side-effects to the
|
||||
/// content view.
|
||||
public let containerView: UIView = UIView()
|
||||
|
||||
@available(*, unavailable, renamed: "containerView")
|
||||
public var backgroundView: UIView!
|
||||
|
||||
private lazy var containerViewTopConstraint = containerView.topAnchor.constraint(equalTo: topAnchor, constant: containerMargins.top)
|
||||
private lazy var containerViewHeightConstraint = containerView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1.0)
|
||||
private lazy var containerViewLeftConstraint = containerView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0)
|
||||
private lazy var containerViewRightConstraint = containerView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0)
|
||||
|
||||
/// The content view top constraint
|
||||
private var contentViewTopConstraint: NSLayoutConstraint?
|
||||
/// The content view left constraint
|
||||
private var contentViewLeftConstraint: NSLayoutConstraint?
|
||||
/// The content right constraint
|
||||
private var contentViewRightConstraint: NSLayoutConstraint?
|
||||
/// The content height constraint
|
||||
private var contentViewHeightConstraint: NSLayoutConstraint?
|
||||
|
||||
private lazy var grabberHandleWidthConstraint = grabberHandle.widthAnchor.constraint(equalToConstant: grabberHandleWidth)
|
||||
private lazy var grabberHandleHeightConstraint = grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandleHeight)
|
||||
private lazy var grabberHandleTopConstraint = grabberHandle.topAnchor.constraint(equalTo: topAnchor, constant: grabberTopPadding)
|
||||
|
||||
public override class var requiresConstraintBasedLayout: Bool { return true }
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
render()
|
||||
addSubViews()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
render()
|
||||
addSubViews()
|
||||
}
|
||||
|
||||
private func render() {
|
||||
private func addSubViews() {
|
||||
super.backgroundColor = .clear
|
||||
self.clipsToBounds = false
|
||||
|
||||
let contentView = FloatingPanelSurfaceContentView()
|
||||
addSubview(contentView)
|
||||
self.contentView = contentView as UIView
|
||||
// contentView.backgroundColor = .lightGray
|
||||
contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(containerView)
|
||||
containerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
contentView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
|
||||
contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0),
|
||||
contentView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0),
|
||||
contentView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
|
||||
containerViewTopConstraint,
|
||||
containerViewLeftConstraint,
|
||||
containerViewRightConstraint,
|
||||
containerViewHeightConstraint,
|
||||
])
|
||||
|
||||
let grabberHandle = GrabberHandleView()
|
||||
addSubview(grabberHandle)
|
||||
self.grabberHandle = grabberHandle
|
||||
|
||||
grabberHandle.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
grabberHandle.topAnchor.constraint(equalTo: topAnchor, constant: Default.grabberTopPadding),
|
||||
grabberHandle.widthAnchor.constraint(equalToConstant: grabberHandle.frame.width),
|
||||
grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandle.frame.height),
|
||||
grabberHandleWidthConstraint,
|
||||
grabberHandleHeightConstraint,
|
||||
grabberHandleTopConstraint,
|
||||
grabberHandle.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
public override func updateConstraints() {
|
||||
containerViewTopConstraint.constant = containerMargins.top
|
||||
containerViewLeftConstraint.constant = containerMargins.left
|
||||
containerViewRightConstraint.constant = -containerMargins.right
|
||||
containerViewHeightConstraint.constant = (containerMargins.bottom == 0) ? bottomOverflow : -(containerMargins.top + containerMargins.bottom)
|
||||
|
||||
contentViewTopConstraint?.constant = containerMargins.top + contentInsets.top
|
||||
contentViewLeftConstraint?.constant = containerMargins.left + contentInsets.left
|
||||
contentViewRightConstraint?.constant = containerMargins.right + contentInsets.right
|
||||
contentViewHeightConstraint?.constant = -(containerMargins.top + containerMargins.bottom + contentInsets.top + contentInsets.bottom)
|
||||
|
||||
grabberHandleTopConstraint.constant = grabberTopPadding
|
||||
grabberHandleWidthConstraint.constant = grabberHandleWidth
|
||||
grabberHandleHeightConstraint.constant = grabberHandleHeight
|
||||
|
||||
super.updateConstraints()
|
||||
}
|
||||
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
updateShadowLayer()
|
||||
// Don't use `contentView.layer.mask` because of UIVisualEffectView issue on ios10, https://forums.developer.apple.com/thread/50854
|
||||
contentView.layer.cornerRadius = cornerRadius
|
||||
contentView.clipsToBounds = true
|
||||
contentView.layer.borderColor = borderColor?.cgColor
|
||||
contentView.layer.borderWidth = borderWidth
|
||||
log.debug("surface view frame = \(frame)")
|
||||
|
||||
containerView.backgroundColor = color
|
||||
|
||||
updateShadow()
|
||||
updateCornerRadius()
|
||||
updateBorder()
|
||||
}
|
||||
|
||||
private func updateShadowLayer() {
|
||||
if shadowLayer != nil {
|
||||
shadowLayer.removeFromSuperlayer()
|
||||
}
|
||||
shadowLayer = makeShadowLayer()
|
||||
layer.insertSublayer(shadowLayer, at: 0)
|
||||
}
|
||||
|
||||
private func makeShadowLayer() -> CAShapeLayer {
|
||||
log.debug("SurfaceView bounds", bounds)
|
||||
let shadowLayer = CAShapeLayer()
|
||||
let path = UIBezierPath(roundedRect: bounds,
|
||||
byRoundingCorners: [.topLeft, .topRight],
|
||||
cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
|
||||
shadowLayer.path = path.cgPath
|
||||
shadowLayer.fillColor = color?.cgColor
|
||||
private func updateShadow() {
|
||||
if shadowHidden == false {
|
||||
shadowLayer.shadowPath = shadowLayer.path
|
||||
shadowLayer.shadowColor = shadowColor.cgColor
|
||||
shadowLayer.shadowOffset = shadowOffset
|
||||
shadowLayer.shadowOpacity = shadowOpacity
|
||||
shadowLayer.shadowRadius = shadowRadius
|
||||
if #available(iOS 11, *) {
|
||||
// For clear background. See also, https://github.com/SCENEE/FloatingPanel/pull/51.
|
||||
layer.shadowColor = shadowColor.cgColor
|
||||
layer.shadowOffset = shadowOffset
|
||||
layer.shadowOpacity = shadowOpacity
|
||||
layer.shadowRadius = shadowRadius
|
||||
} else {
|
||||
// Can't update `layer.shadow*` directly because of a UIVisualEffectView issue in iOS 10, https://forums.developer.apple.com/thread/50854
|
||||
// Instead, a user should display shadow appropriately.
|
||||
}
|
||||
}
|
||||
return shadowLayer
|
||||
}
|
||||
|
||||
private func updateCornerRadius() {
|
||||
guard containerView.layer.cornerRadius != 0.0 else {
|
||||
containerView.layer.masksToBounds = false
|
||||
return
|
||||
}
|
||||
containerView.layer.masksToBounds = true
|
||||
guard containerMargins.bottom == 0 else { return }
|
||||
if #available(iOS 11, *) {
|
||||
// Don't use `contentView.clipToBounds` because it prevents content view from expanding the height of a subview of it
|
||||
// for the bottom overflow like Auto Layout settings of UIVisualEffectView in Main.storyboard of Example/Maps.
|
||||
// Because the bottom of contentView must be fit to the bottom of a screen to work the `safeLayoutGuide` of a content VC.
|
||||
containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
} else {
|
||||
// Can't use `containerView.layer.mask` because of a UIVisualEffectView issue in iOS 10, https://forums.developer.apple.com/thread/50854
|
||||
// Instead, a user should display rounding corners appropriately.
|
||||
}
|
||||
}
|
||||
|
||||
private func updateBorder() {
|
||||
containerView.layer.borderColor = borderColor?.cgColor
|
||||
containerView.layer.borderWidth = borderWidth
|
||||
}
|
||||
|
||||
func add(contentView: UIView) {
|
||||
containerView.addSubview(contentView)
|
||||
self.contentView = contentView
|
||||
/* contentView.frame = bounds */ // MUST NOT: Because the top safe area inset of a content VC will be incorrect.
|
||||
contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let topConstraint = contentView.topAnchor.constraint(equalTo: topAnchor, constant: containerMargins.top + contentInsets.top)
|
||||
let leftConstraint = contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: containerMargins.left + contentInsets.left)
|
||||
let rightConstraint = rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: containerMargins.right + contentInsets.right)
|
||||
let heightPadding = containerMargins.top + containerMargins.bottom + contentInsets.top + contentInsets.bottom
|
||||
let heightConstraint = contentView.heightAnchor.constraint(equalTo: heightAnchor, constant: -heightPadding)
|
||||
NSLayoutConstraint.activate([
|
||||
topConstraint,
|
||||
leftConstraint,
|
||||
rightConstraint,
|
||||
heightConstraint,
|
||||
])
|
||||
self.contentViewTopConstraint = topConstraint
|
||||
self.contentViewLeftConstraint = leftConstraint
|
||||
self.contentViewRightConstraint = rightConstraint
|
||||
self.contentViewHeightConstraint = heightConstraint
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/11/21.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class FloatingPanelModalTransition: NSObject, UIViewControllerTransitioningDelegate {
|
||||
func animationController(forPresented presented: UIViewController,
|
||||
presenting: UIViewController,
|
||||
source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||
return FloatingPanelModalPresentTransition()
|
||||
}
|
||||
|
||||
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||
return FloatingPanelModalDismissTransition()
|
||||
}
|
||||
|
||||
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
|
||||
return FloatingPanelPresentationController(presentedViewController: presented, presenting: presenting)
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingPanelPresentationController: UIPresentationController {
|
||||
override func presentationTransitionWillBegin() {
|
||||
// Must call here even if duplicating on in containerViewWillLayoutSubviews()
|
||||
// Because it let the floating panel present correctly with the presentation animation
|
||||
addFloatingPanel()
|
||||
}
|
||||
|
||||
override func presentationTransitionDidEnd(_ completed: Bool) {
|
||||
// For non-animated presentation
|
||||
if let fpc = presentedViewController as? FloatingPanelController, fpc.position == .hidden {
|
||||
fpc.show(animated: false, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
override func dismissalTransitionDidEnd(_ completed: Bool) {
|
||||
if let fpc = presentedViewController as? FloatingPanelController {
|
||||
// For non-animated dismissal
|
||||
if fpc.position != .hidden {
|
||||
fpc.hide(animated: false, completion: nil)
|
||||
}
|
||||
fpc.view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
override func containerViewWillLayoutSubviews() {
|
||||
guard
|
||||
let fpc = presentedViewController as? FloatingPanelController
|
||||
else { fatalError() }
|
||||
|
||||
/*
|
||||
* Layout the views managed by `FloatingPanelController` here for the
|
||||
* sake of the presentation and dismissal modally from the controller.
|
||||
*/
|
||||
addFloatingPanel()
|
||||
|
||||
// Forward touch events to the presenting view controller
|
||||
(fpc.view as? FloatingPanelPassThroughView)?.eventForwardingView = presentingViewController.view
|
||||
|
||||
fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true
|
||||
}
|
||||
|
||||
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
|
||||
presentedViewController.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func addFloatingPanel() {
|
||||
guard
|
||||
let containerView = self.containerView,
|
||||
let fpc = presentedViewController as? FloatingPanelController
|
||||
else { fatalError() }
|
||||
|
||||
containerView.addSubview(fpc.view)
|
||||
fpc.view.frame = containerView.bounds
|
||||
fpc.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingPanelModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||
guard
|
||||
let fpc = transitionContext?.viewController(forKey: .to) as? FloatingPanelController
|
||||
else { fatalError()}
|
||||
|
||||
let animator = fpc.behavior.addAnimator(fpc, to: fpc.layout.initialPosition)
|
||||
return TimeInterval(animator.duration)
|
||||
}
|
||||
|
||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
guard
|
||||
let fpc = transitionContext.viewController(forKey: .to) as? FloatingPanelController
|
||||
else { fatalError() }
|
||||
|
||||
fpc.show(animated: true) {
|
||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingPanelModalDismissTransition: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||
guard
|
||||
let fpc = transitionContext?.viewController(forKey: .from) as? FloatingPanelController
|
||||
else { fatalError()}
|
||||
|
||||
let animator = fpc.behavior.removeAnimator(fpc, from: fpc.position)
|
||||
return TimeInterval(animator.duration)
|
||||
}
|
||||
|
||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
guard
|
||||
let fpc = transitionContext.viewController(forKey: .from) as? FloatingPanelController
|
||||
else { fatalError() }
|
||||
|
||||
fpc.hide(animated: true) {
|
||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/11/21.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class FloatingPanelPassThroughView: UIView {
|
||||
public weak var eventForwardingView: UIView?
|
||||
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let hitView = super.hitTest(point, with: event)
|
||||
switch hitView {
|
||||
case self:
|
||||
return eventForwardingView?.hitTest(self.convert(point, to: eventForwardingView), with: event)
|
||||
default:
|
||||
return hitView
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,33 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/09/19.
|
||||
// Copyright © 2018 scenee. All rights reserved.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
public class GrabberHandleView: UIView {
|
||||
public struct Default {
|
||||
public static let width: CGFloat = 36.0
|
||||
public static let height: CGFloat = 5.0
|
||||
public static let barColor = UIColor(displayP3Red: 0.76, green: 0.77, blue: 0.76, alpha: 1.0)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
public var barColor = UIColor(displayP3Red: 0.76, green: 0.77, blue: 0.76, alpha: 1.0) { didSet { backgroundColor = barColor } }
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
render()
|
||||
}
|
||||
|
||||
init() {
|
||||
let size = CGSize(width: Default.width,
|
||||
height: Default.height)
|
||||
super.init(frame: CGRect(origin: .zero, size: size))
|
||||
self.backgroundColor = Default.barColor
|
||||
super.init(frame: .zero)
|
||||
backgroundColor = barColor
|
||||
}
|
||||
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
render()
|
||||
}
|
||||
|
||||
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let view = super.hitTest(point, with: event)
|
||||
return view == self ? nil : view
|
||||
}
|
||||
|
||||
private func render() {
|
||||
self.layer.masksToBounds = true
|
||||
self.layer.cornerRadius = frame.size.height * 0.5
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>1.7.2</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
</dict>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/10/09.
|
||||
// Copyright © 2018 scenee. All rights reserved.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@@ -19,29 +19,17 @@ struct Logger {
|
||||
case info = 1
|
||||
case warning = 2
|
||||
case error = 3
|
||||
case fault = 4
|
||||
|
||||
var name: String {
|
||||
switch self {
|
||||
case .debug: return "DEBUG"
|
||||
case .info: return "INFO"
|
||||
case .warning: return "WARNING"
|
||||
case .error: return "ERROR"
|
||||
case .fault: return "FAULT"
|
||||
}
|
||||
}
|
||||
var shortName: String {
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .debug:
|
||||
return "D/"
|
||||
case .info:
|
||||
return "I/"
|
||||
case .warning:
|
||||
return "W/"
|
||||
return "Warning:"
|
||||
case .error:
|
||||
return "E/"
|
||||
case .fault:
|
||||
return "F/"
|
||||
return "Error:"
|
||||
}
|
||||
}
|
||||
@available(iOS 10.0, *)
|
||||
@@ -49,31 +37,40 @@ struct Logger {
|
||||
switch self {
|
||||
case .debug: return .debug
|
||||
case .info: return .info
|
||||
case .warning: return .info
|
||||
case .warning: return .default
|
||||
case .error: return .error
|
||||
case .fault: return .fault
|
||||
}
|
||||
}
|
||||
|
||||
public static func < (lhs: Logger.Level, rhs: Logger.Level) -> Bool {
|
||||
static func < (lhs: Logger.Level, rhs: Logger.Level) -> Bool {
|
||||
return lhs.rawValue < rhs.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
typealias Hook = ((String, Level) -> Void)
|
||||
var hook: Hook?
|
||||
|
||||
fileprivate init() {
|
||||
osLog = OSLog(subsystem: "com.scenee.FloatingPanel", category: "FloatingPanel")
|
||||
}
|
||||
|
||||
private func log(_ level: Level, _ message: Any, _ arguments: [Any], function: String, line: UInt) {
|
||||
#if __FP_LOG
|
||||
_ = s.wait(timeout: .now() + 0.033)
|
||||
defer { s.signal() }
|
||||
|
||||
let extraMessage: String = arguments.map({ String(describing: $0) }).joined(separator: " ")
|
||||
let log = "\(level.shortName) \(message) \(extraMessage) (\(function):\(line))"
|
||||
let log: String = {
|
||||
switch level {
|
||||
case .debug:
|
||||
return "\(level.displayName) \(message) \(extraMessage) (\(function):\(line))"
|
||||
default:
|
||||
return "\(level.displayName) \(message) \(extraMessage)"
|
||||
}
|
||||
}()
|
||||
|
||||
hook?(log, level)
|
||||
|
||||
os_log("%@", log: osLog, type: level.osLogType, log)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func getPrettyFunction(_ function: String, _ file: String) -> String {
|
||||
@@ -85,7 +82,9 @@ struct Logger {
|
||||
}
|
||||
|
||||
func debug(_ log: Any, _ arguments: Any..., function: String = #function, file: String = #file, line: UInt = #line) {
|
||||
#if __FP_LOG
|
||||
self.log(.debug, log, arguments, function: getPrettyFunction(function, file), line: line)
|
||||
#endif
|
||||
}
|
||||
|
||||
func info(_ log: Any, _ arguments: Any..., function: String = #function, file: String = #file, line: UInt = #line) {
|
||||
@@ -99,8 +98,4 @@ struct Logger {
|
||||
func error(_ log: Any, _ arguments: Any..., function: String = #function, file: String = #file, line: UInt = #line) {
|
||||
self.log(.error, log, arguments, function: getPrettyFunction(function, file), line: line)
|
||||
}
|
||||
|
||||
func fault(_ log: Any, _ arguments: Any..., function: String = #function, file: String = #file, line: UInt = #line) {
|
||||
self.log(.fault, log, arguments, function: getPrettyFunction(function, file), line: line)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/09/18.
|
||||
// Copyright © 2018 scenee. All rights reserved.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
@@ -21,7 +21,7 @@ class CustomLayoutGuide: LayoutGuideProvider {
|
||||
}
|
||||
|
||||
extension UIViewController {
|
||||
var layoutInsets: UIEdgeInsets {
|
||||
@objc var layoutInsets: UIEdgeInsets {
|
||||
if #available(iOS 11.0, *) {
|
||||
return view.safeAreaInsets
|
||||
} else {
|
||||
@@ -50,6 +50,8 @@ protocol SideLayoutGuideProvider {
|
||||
extension UIView: SideLayoutGuideProvider {}
|
||||
extension UILayoutGuide: SideLayoutGuideProvider {}
|
||||
|
||||
// The reason why UIView has no extensions of safe area insets and top/bottom guides
|
||||
// is for iOS10 compat.
|
||||
extension UIView {
|
||||
var sideLayoutGuide: SideLayoutGuideProvider {
|
||||
if #available(iOS 11.0, *) {
|
||||
@@ -58,23 +60,100 @@ extension UIView {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
var presentationFrame: CGRect {
|
||||
return layer.presentation()?.frame ?? frame
|
||||
}
|
||||
}
|
||||
|
||||
extension UIView {
|
||||
func disableAutoLayout() {
|
||||
let frame = self.frame
|
||||
translatesAutoresizingMaskIntoConstraints = true
|
||||
self.frame = frame
|
||||
}
|
||||
func enableAutoLayout() {
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
}
|
||||
|
||||
static func performWithLinear(startTime: Double = 0.0, relativeDuration: Double = 1.0, _ animations: @escaping (() -> Void)) {
|
||||
UIView.animateKeyframes(withDuration: 0.0, delay: 0.0, options: [.calculationModeCubic], animations: {
|
||||
UIView.addKeyframe(withRelativeStartTime: startTime, relativeDuration: relativeDuration, animations: animations)
|
||||
}, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
#if __FP_LOG
|
||||
#if swift(>=4.2)
|
||||
extension UIGestureRecognizer.State: CustomDebugStringConvertible {
|
||||
public var debugDescription: String {
|
||||
switch self {
|
||||
case .began: return "Began"
|
||||
case .changed: return "Changed"
|
||||
case .failed: return "Failed"
|
||||
case .cancelled: return "Cancelled"
|
||||
case .ended: return "Endeded"
|
||||
case .possible: return "Possible"
|
||||
case .began: return "began"
|
||||
case .changed: return "changed"
|
||||
case .failed: return "failed"
|
||||
case .cancelled: return "cancelled"
|
||||
case .ended: return "endeded"
|
||||
case .possible: return "possible"
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
extension UIGestureRecognizerState: CustomDebugStringConvertible {
|
||||
public var debugDescription: String {
|
||||
switch self {
|
||||
case .began: return "began"
|
||||
case .changed: return "changed"
|
||||
case .failed: return "failed"
|
||||
case .cancelled: return "cancelled"
|
||||
case .ended: return "endeded"
|
||||
case .possible: return "possible"
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
extension UIScrollView {
|
||||
var contentOffsetZero: CGPoint {
|
||||
return CGPoint(x: 0.0, y: 0.0 + contentInset.top)
|
||||
return CGPoint(x: 0.0, y: 0.0 - contentInset.top)
|
||||
}
|
||||
var isLocked: Bool {
|
||||
return !showsVerticalScrollIndicator && !bounces && isDirectionalLockEnabled
|
||||
}
|
||||
}
|
||||
|
||||
extension UISpringTimingParameters {
|
||||
public convenience init(dampingRatio: CGFloat, frequencyResponse: CGFloat, initialVelocity: CGVector = .zero) {
|
||||
let mass = 1 as CGFloat
|
||||
let stiffness = pow(2 * .pi / frequencyResponse, 2) * mass
|
||||
let damp = 4 * .pi * dampingRatio * mass / frequencyResponse
|
||||
self.init(mass: mass, stiffness: stiffness, damping: damp, initialVelocity: initialVelocity)
|
||||
}
|
||||
}
|
||||
|
||||
extension CGPoint {
|
||||
static var nan: CGPoint {
|
||||
return CGPoint(x: CGFloat.nan,
|
||||
y: CGFloat.nan)
|
||||
}
|
||||
}
|
||||
|
||||
extension UITraitCollection {
|
||||
func shouldUpdateLayout(from previous: UITraitCollection) -> Bool {
|
||||
return previous.horizontalSizeClass != horizontalSizeClass
|
||||
|| previous.verticalSizeClass != verticalSizeClass
|
||||
|| previous.preferredContentSizeCategory != preferredContentSizeCategory
|
||||
|| previous.layoutDirection != layoutDirection
|
||||
}
|
||||
}
|
||||
|
||||
extension NSLayoutConstraint {
|
||||
static func activate(constraint: NSLayoutConstraint?) {
|
||||
guard let constraint = constraint else { return }
|
||||
self.activate([constraint])
|
||||
}
|
||||
static func deactivate(constraint: NSLayoutConstraint?) {
|
||||
guard let constraint = constraint else { return }
|
||||
self.deactivate([constraint])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/11/01.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
let rootVC = UIViewController(nibName: nil, bundle: nil)
|
||||
rootVC.view.backgroundColor = .gray
|
||||
|
||||
let window = UIWindow()
|
||||
window.rootViewController = rootVC
|
||||
window.makeKeyAndVisible()
|
||||
self.window = window
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13142" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12042"/>
|
||||
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Copyright © 2019 scenee. All rights reserved." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="obG-Y5-kRd">
|
||||
<rect key="frame" x="0.0" y="626.5" width="375" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="TestingApp" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="GJd-Yh-RWb">
|
||||
<rect key="frame" x="0.0" y="202" width="375" height="43"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="36"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Bcu-3y-fUS" firstAttribute="centerX" secondItem="obG-Y5-kRd" secondAttribute="centerX" id="5cz-MP-9tL"/>
|
||||
<constraint firstItem="Bcu-3y-fUS" firstAttribute="centerX" secondItem="GJd-Yh-RWb" secondAttribute="centerX" id="Q3B-4B-g5h"/>
|
||||
<constraint firstItem="obG-Y5-kRd" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" constant="20" symbolic="YES" id="SfN-ll-jLj"/>
|
||||
<constraint firstAttribute="bottom" secondItem="obG-Y5-kRd" secondAttribute="bottom" constant="20" id="Y44-ml-fuU"/>
|
||||
<constraint firstItem="GJd-Yh-RWb" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="bottom" multiplier="1/3" constant="1" id="moa-c2-u7t"/>
|
||||
<constraint firstItem="GJd-Yh-RWb" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" constant="20" symbolic="YES" id="x7j-FC-K8j"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,188 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/09/18.
|
||||
// Copyright © 2018 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import FloatingPanel
|
||||
|
||||
class FloatingPanelControllerTests: XCTestCase {
|
||||
override func setUp() {}
|
||||
override func tearDown() {}
|
||||
|
||||
func test_warningRetainCycle() {
|
||||
let myVC = MyZombieViewController(nibName: nil, bundle: nil)
|
||||
let exp = expectation(description: "Warning retain cycle")
|
||||
exp.expectedFulfillmentCount = 2 // For layout & behavior logs
|
||||
log.hook = {(log, level) in
|
||||
if log.contains("A memory leak will occur by a retain cycle because") {
|
||||
XCTAssert(level == .warning)
|
||||
exp.fulfill()
|
||||
}
|
||||
}
|
||||
myVC.loadViewIfNeeded()
|
||||
wait(for: [exp], timeout: 10)
|
||||
}
|
||||
|
||||
func test_addPanel() {
|
||||
guard let rootVC = UIApplication.shared.keyWindow?.rootViewController else { fatalError() }
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.addPanel(toParent: rootVC)
|
||||
XCTAssert(fpc.surfaceView.frame.minY == (fpc.view.bounds.height - fpc.layoutInsets.bottom) - fpc.layout.insetFor(position: .half)!)
|
||||
fpc.move(to: .tip, animated: false)
|
||||
XCTAssert(fpc.surfaceView.frame.minY == (fpc.view.bounds.height - fpc.layoutInsets.bottom) - fpc.layout.insetFor(position: .tip)!)
|
||||
}
|
||||
|
||||
@available(iOS 12.0, *)
|
||||
func test_updateLayout_willTransition() {
|
||||
class MyDelegate: FloatingPanelControllerDelegate {
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
|
||||
if newCollection.userInterfaceStyle == .dark {
|
||||
XCTFail()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
let myDelegate = MyDelegate()
|
||||
let fpc = FloatingPanelController(delegate: myDelegate)
|
||||
let traitCollection = UITraitCollection(traitsFrom: [fpc.traitCollection,
|
||||
UITraitCollection(userInterfaceStyle: .dark)])
|
||||
XCTAssertEqual(traitCollection.userInterfaceStyle, .dark)
|
||||
fpc.prepare(for: traitCollection)
|
||||
}
|
||||
|
||||
func test_moveTo() {
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
XCTAssertEqual(delegate.position, .hidden)
|
||||
fpc.showForTest()
|
||||
XCTAssertEqual(delegate.position, .half)
|
||||
|
||||
fpc.hide()
|
||||
XCTAssertEqual(delegate.position, .hidden)
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
XCTAssertEqual(fpc.position, .full)
|
||||
XCTAssertEqual(delegate.position, .full)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .full))
|
||||
|
||||
fpc.move(to: .half, animated: false)
|
||||
XCTAssertEqual(fpc.position, .half)
|
||||
XCTAssertEqual(delegate.position, .half)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .half))
|
||||
|
||||
fpc.move(to: .tip, animated: false)
|
||||
XCTAssertEqual(fpc.position, .tip)
|
||||
XCTAssertEqual(delegate.position, .tip)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .tip))
|
||||
|
||||
fpc.move(to: .hidden, animated: false)
|
||||
XCTAssertEqual(fpc.position, .hidden)
|
||||
XCTAssertEqual(delegate.position, .hidden)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .hidden))
|
||||
|
||||
fpc.move(to: .full, animated: true)
|
||||
XCTAssertEqual(fpc.position, .full)
|
||||
XCTAssertEqual(delegate.position, .full)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .full))
|
||||
|
||||
fpc.move(to: .half, animated: true)
|
||||
XCTAssertEqual(fpc.position, .half)
|
||||
XCTAssertEqual(delegate.position, .half)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .half))
|
||||
|
||||
fpc.move(to: .tip, animated: true)
|
||||
XCTAssertEqual(fpc.position, .tip)
|
||||
XCTAssertEqual(delegate.position, .tip)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .tip))
|
||||
|
||||
fpc.move(to: .hidden, animated: true)
|
||||
XCTAssertEqual(fpc.position, .hidden)
|
||||
XCTAssertEqual(delegate.position, .hidden)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .hidden))
|
||||
}
|
||||
|
||||
func test_moveWithNearbyPosition() {
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
XCTAssertEqual(delegate.position, .hidden)
|
||||
fpc.showForTest()
|
||||
|
||||
XCTAssertEqual(fpc.nearbyPosition, .half)
|
||||
|
||||
fpc.hide()
|
||||
XCTAssertEqual(fpc.nearbyPosition, .tip)
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
XCTAssertEqual(fpc.nearbyPosition, .full)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .full))
|
||||
}
|
||||
|
||||
func test_originSurfaceY() {
|
||||
let fpc = FloatingPanelController(delegate: nil)
|
||||
fpc.loadViewIfNeeded()
|
||||
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
|
||||
fpc.show(animated: false, completion: nil)
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .full))
|
||||
fpc.move(to: .half, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .half))
|
||||
fpc.move(to: .tip, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .tip))
|
||||
fpc.move(to: .hidden, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .hidden))
|
||||
}
|
||||
|
||||
func test_contentMode() {
|
||||
let fpc = FloatingPanelController(delegate: nil)
|
||||
fpc.loadViewIfNeeded()
|
||||
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
|
||||
fpc.show(animated: false, completion: nil)
|
||||
|
||||
fpc.contentMode = .static
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .full))
|
||||
fpc.move(to: .half, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .full))
|
||||
fpc.move(to: .tip, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .full))
|
||||
|
||||
fpc.contentMode = .fitToBounds
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .full))
|
||||
fpc.move(to: .half, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .half))
|
||||
fpc.move(to: .tip, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .tip))
|
||||
}
|
||||
}
|
||||
|
||||
private class MyZombieViewController: UIViewController, FloatingPanelLayout, FloatingPanelBehavior, FloatingPanelControllerDelegate {
|
||||
var fpc: FloatingPanelController?
|
||||
override func viewDidLoad() {
|
||||
fpc = FloatingPanelController(delegate: self)
|
||||
fpc?.addPanel(toParent: self)
|
||||
}
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
|
||||
return self
|
||||
}
|
||||
|
||||
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
|
||||
return self
|
||||
}
|
||||
var initialPosition: FloatingPanelPosition {
|
||||
return .half
|
||||
}
|
||||
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
|
||||
switch position {
|
||||
case .full: return UIScreen.main.bounds.height == 667.0 ? 18.0 : 16.0
|
||||
case .half: return 262.0
|
||||
case .tip: return 69.0
|
||||
case .hidden: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2019/06/27.
|
||||
// Copyright © 2019 scenee. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import FloatingPanel
|
||||
|
||||
class FloatingPanelLayoutTests: XCTestCase {
|
||||
var fpc: FloatingPanelController!
|
||||
override func setUp() {
|
||||
fpc = FloatingPanelController(delegate: nil)
|
||||
fpc.loadViewIfNeeded()
|
||||
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
|
||||
}
|
||||
override func tearDown() {}
|
||||
|
||||
func test_layoutAdapter_topAndBottomMostState() {
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.topMostState, .full)
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.bottomMostState, .tip)
|
||||
|
||||
class FloatingPanelLayoutWithHidden: FloatingPanelLayout {
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat? { return nil }
|
||||
let initialPosition: FloatingPanelPosition = .hidden
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .half, .full]
|
||||
}
|
||||
class FloatingPanelLayout2Positions: FloatingPanelLayout {
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat? { return nil }
|
||||
let initialPosition: FloatingPanelPosition = .tip
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.tip, .half]
|
||||
}
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayoutWithHidden()
|
||||
fpc.delegate = delegate
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.topMostState, .full)
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.bottomMostState, .hidden)
|
||||
|
||||
delegate.layout = FloatingPanelLayout2Positions()
|
||||
fpc.delegate = delegate
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.topMostState, .half)
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.bottomMostState, .tip)
|
||||
}
|
||||
|
||||
func test_layoutSegment_3position() {
|
||||
class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .half
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.tip, .half, .full]
|
||||
}
|
||||
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout3Positions()
|
||||
fpc.delegate = delegate
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let halfPos = fpc.originYOfSurface(for: .half)
|
||||
let tipPos = fpc.originYOfSurface(for: .tip)
|
||||
|
||||
let minPos = CGFloat.leastNormalMagnitude
|
||||
let maxPos = CGFloat.greatestFiniteMagnitude
|
||||
|
||||
assertLayoutSegment(fpc.floatingPanel, with: [
|
||||
(#line, pos: minPos, forwardY: true, lower: nil, upper: .full),
|
||||
(#line, pos: minPos, forwardY: false, lower: nil, upper: .full),
|
||||
(#line, pos: fullPos, forwardY: true, lower: .full, upper: .half),
|
||||
(#line, pos: fullPos, forwardY: false, lower: nil, upper: .full),
|
||||
(#line, pos: halfPos, forwardY: true, lower: .half, upper: .tip),
|
||||
(#line, pos: halfPos, forwardY: false, lower: .full, upper: .half),
|
||||
(#line, pos: tipPos, forwardY: true, lower: .tip, upper: nil),
|
||||
(#line, pos: tipPos, forwardY: false, lower: .half, upper: .tip),
|
||||
(#line, pos: maxPos, forwardY: true, lower: .tip, upper: nil),
|
||||
(#line, pos: maxPos, forwardY: false, lower: .tip, upper: nil),
|
||||
])
|
||||
}
|
||||
|
||||
func test_layoutSegment_2positions() {
|
||||
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .half
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.half, .full]
|
||||
}
|
||||
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout2Positions()
|
||||
fpc.delegate = delegate
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let halfPos = fpc.originYOfSurface(for: .half)
|
||||
|
||||
let minPos = CGFloat.leastNormalMagnitude
|
||||
let maxPos = CGFloat.greatestFiniteMagnitude
|
||||
|
||||
assertLayoutSegment(fpc.floatingPanel, with: [
|
||||
(#line, pos: minPos, forwardY: true, lower: nil, upper: .full),
|
||||
(#line, pos: minPos, forwardY: false, lower: nil, upper: .full),
|
||||
(#line, pos: fullPos, forwardY: true, lower: .full, upper: .half),
|
||||
(#line, pos: fullPos, forwardY: false, lower: nil, upper: .full),
|
||||
(#line, pos: halfPos, forwardY: true, lower: .half, upper: nil),
|
||||
(#line, pos: halfPos, forwardY: false, lower: .full, upper: .half),
|
||||
(#line, pos: maxPos, forwardY: true, lower: .half, upper: nil),
|
||||
(#line, pos: maxPos, forwardY: false, lower: .half, upper: nil),
|
||||
])
|
||||
}
|
||||
|
||||
func test_layoutSegment_1positions() {
|
||||
class FloatingPanelLayout1Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .full
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.full]
|
||||
}
|
||||
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout1Positions()
|
||||
fpc.delegate = delegate
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
|
||||
let minPos = CGFloat.leastNormalMagnitude
|
||||
let maxPos = CGFloat.greatestFiniteMagnitude
|
||||
|
||||
assertLayoutSegment(fpc.floatingPanel, with: [
|
||||
(#line, pos: minPos, forwardY: true, lower: nil, upper: .full),
|
||||
(#line, pos: minPos, forwardY: false, lower: nil, upper: .full),
|
||||
(#line, pos: fullPos, forwardY: true, lower: .full, upper: nil),
|
||||
(#line, pos: fullPos, forwardY: false, lower: nil, upper: .full),
|
||||
(#line, pos: maxPos, forwardY: true, lower: .full, upper: nil),
|
||||
(#line, pos: maxPos, forwardY: false, lower: .full, upper: nil),
|
||||
])
|
||||
}
|
||||
|
||||
func test_updateInteractiveTopConstraint() {
|
||||
fpc.showForTest()
|
||||
fpc.move(to: .full, animated: false)
|
||||
|
||||
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.position)
|
||||
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.position) // Should be ignore
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let tipPos = fpc.originYOfSurface(for: .tip)
|
||||
|
||||
var pre: CGFloat
|
||||
var next: CGFloat
|
||||
pre = fpc.surfaceView.frame.minY
|
||||
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: -100.0, allowsTopBuffer: false, with: fpc.behavior)
|
||||
next = fpc.surfaceView.frame.minY
|
||||
XCTAssertEqual(next, pre)
|
||||
|
||||
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: -100.0, allowsTopBuffer: true, with: fpc.behavior)
|
||||
next = fpc.surfaceView.frame.minY
|
||||
XCTAssertEqual(next, fullPos - fpc.layout.topInteractionBuffer)
|
||||
|
||||
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: 100.0, allowsTopBuffer: true, with: fpc.behavior)
|
||||
next = fpc.surfaceView.frame.minY
|
||||
XCTAssertEqual(next, fullPos + 100.0)
|
||||
|
||||
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: tipPos - fullPos, allowsTopBuffer: true, with: fpc.behavior)
|
||||
next = fpc.surfaceView.frame.minY
|
||||
XCTAssertEqual(next, tipPos)
|
||||
|
||||
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: tipPos - fullPos + 100.0, allowsTopBuffer: true, with: fpc.behavior)
|
||||
next = fpc.surfaceView.frame.minY
|
||||
XCTAssertEqual(next, tipPos + fpc.layout.bottomInteractionBuffer)
|
||||
|
||||
fpc.floatingPanel.layoutAdapter.endInteraction(at: fpc.position)
|
||||
}
|
||||
|
||||
func test_updateInteractiveTopConstraintWithHidden() {
|
||||
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .hidden
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .full]
|
||||
}
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout2Positions()
|
||||
fpc.delegate = delegate
|
||||
fpc.showForTest()
|
||||
fpc.move(to: .full, animated: false)
|
||||
|
||||
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.position)
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let hiddenPos = fpc.originYOfSurface(for: .hidden)
|
||||
|
||||
var pre: CGFloat
|
||||
var next: CGFloat
|
||||
pre = fpc.surfaceView.frame.minY
|
||||
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: -100.0, allowsTopBuffer: false, with: fpc.behavior)
|
||||
next = fpc.surfaceView.frame.minY
|
||||
XCTAssertEqual(next, pre)
|
||||
|
||||
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: -100.0, allowsTopBuffer: true, with: fpc.behavior)
|
||||
next = fpc.surfaceView.frame.minY
|
||||
XCTAssertEqual(next, fullPos - fpc.layout.topInteractionBuffer)
|
||||
|
||||
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: hiddenPos - fullPos + 100.0, allowsTopBuffer: true, with: fpc.behavior)
|
||||
next = fpc.surfaceView.frame.minY
|
||||
XCTAssertEqual(next, hiddenPos + fpc.layout.bottomInteractionBuffer)
|
||||
|
||||
fpc.floatingPanel.layoutAdapter.endInteraction(at: fpc.position)
|
||||
}
|
||||
|
||||
func test_positionReference() {
|
||||
fpc = CustomSafeAreaFloatingPanelController()
|
||||
fpc.loadViewIfNeeded()
|
||||
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
|
||||
|
||||
class MyFloatingPanelFullLayout: FloatingPanelTestLayout {
|
||||
var initialPosition: FloatingPanelPosition = .half
|
||||
var positionReference: FloatingPanelLayoutReference {
|
||||
return .fromSuperview
|
||||
}
|
||||
}
|
||||
class MyFloatingPanelSafeAreaLayout: FloatingPanelTestLayout {
|
||||
var initialPosition: FloatingPanelPosition = .half
|
||||
var positionReference: FloatingPanelLayoutReference {
|
||||
return .fromSafeArea
|
||||
}
|
||||
}
|
||||
let fullLayout = MyFloatingPanelFullLayout()
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = fullLayout
|
||||
fpc.delegate = delegate
|
||||
fpc.showForTest()
|
||||
|
||||
XCTAssertEqual(fpc.layout.positionReference, .fromSuperview)
|
||||
XCTAssertEqual(fpc.originYOfSurface(for: .full), fullLayout.insetFor(position: .full))
|
||||
XCTAssertEqual(fpc.originYOfSurface(for: .half), fpc.view!.frame.height - fullLayout.insetFor(position: .half)!)
|
||||
XCTAssertEqual(fpc.originYOfSurface(for: .tip), fpc.view!.frame.height - fullLayout.insetFor(position: .tip)!)
|
||||
|
||||
let safeAreaLayout = MyFloatingPanelSafeAreaLayout()
|
||||
delegate.layout = safeAreaLayout
|
||||
fpc.delegate = delegate
|
||||
|
||||
XCTAssertEqual(fpc.layout.positionReference, .fromSafeArea)
|
||||
XCTAssertEqual(fpc.originYOfSurface(for: .full),
|
||||
fullLayout.insetFor(position: .full)! + fpc.layoutInsets.top)
|
||||
XCTAssertEqual(fpc.originYOfSurface(for: .half),
|
||||
fpc.view!.frame.height - (fullLayout.insetFor(position: .half)! + fpc.layoutInsets.bottom))
|
||||
XCTAssertEqual(fpc.originYOfSurface(for: .tip),
|
||||
fpc.view!.frame.height - (fullLayout.insetFor(position: .tip)! + fpc.layoutInsets.bottom))
|
||||
}
|
||||
}
|
||||
|
||||
private typealias LayoutSegmentTestParameter = (UInt, pos: CGFloat, forwardY: Bool, lower: FloatingPanelPosition?, upper: FloatingPanelPosition?)
|
||||
private func assertLayoutSegment(_ floatingPanel: FloatingPanelCore, with params: [LayoutSegmentTestParameter]) {
|
||||
params.forEach { (line, pos, forwardY, lowr, upper) in
|
||||
let segument = floatingPanel.layoutAdapter.segument(at: pos, forward: forwardY)
|
||||
XCTAssertEqual(segument.lower, lowr, line: line)
|
||||
XCTAssertEqual(segument.upper, upper, line: line)
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomSafeAreaFloatingPanelController: FloatingPanelController { }
|
||||
extension CustomSafeAreaFloatingPanelController {
|
||||
override var layoutInsets: UIEdgeInsets {
|
||||
return UIEdgeInsets(top: 64.0, left: 0.0, bottom: 0.0, right: 34.0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2019/07/05.
|
||||
// Copyright © 2019 scenee. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import FloatingPanel
|
||||
|
||||
class FloatingPanelPositionTests: XCTestCase {
|
||||
override func setUp() { }
|
||||
override func tearDown() { }
|
||||
|
||||
func test_nextAndPre() {
|
||||
var positions: [FloatingPanelPosition]
|
||||
positions = [.full, .half, .tip, .hidden]
|
||||
XCTAssertEqual(FloatingPanelPosition.full.next(in: positions), .half)
|
||||
XCTAssertEqual(FloatingPanelPosition.full.pre(in: positions), .full)
|
||||
XCTAssertEqual(FloatingPanelPosition.hidden.next(in: positions), .hidden)
|
||||
XCTAssertEqual(FloatingPanelPosition.hidden.pre(in: positions), .tip)
|
||||
|
||||
positions = [.full, .hidden]
|
||||
XCTAssertEqual(FloatingPanelPosition.full.next(in: positions), .hidden)
|
||||
XCTAssertEqual(FloatingPanelPosition.full.pre(in: positions), .full)
|
||||
XCTAssertEqual(FloatingPanelPosition.hidden.next(in: positions), .hidden)
|
||||
XCTAssertEqual(FloatingPanelPosition.hidden.pre(in: positions), .full)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2019/05/23.
|
||||
// Copyright © 2019 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import FloatingPanel
|
||||
|
||||
class FloatingPanelSurfaceViewTests: XCTestCase {
|
||||
override func setUp() {}
|
||||
override func tearDown() {}
|
||||
|
||||
func test_surfaceView() {
|
||||
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
|
||||
XCTAssertTrue(FloatingPanelSurfaceView.requiresConstraintBasedLayout)
|
||||
XCTAssert(surface.contentView == nil)
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssert(surface.grabberHandle.frame.minY == 6.0)
|
||||
XCTAssert(surface.grabberHandle.frame.width == surface.grabberHandleWidth)
|
||||
XCTAssert(surface.grabberHandle.frame.height == surface.grabberHandleHeight)
|
||||
surface.backgroundColor = .red
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssert(surface.backgroundColor == surface.containerView.backgroundColor)
|
||||
}
|
||||
|
||||
func test_surfaceView_grabberHandle() {
|
||||
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
|
||||
XCTAssert(surface.contentView == nil)
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssert(surface.grabberHandle.frame.minY == 6.0)
|
||||
XCTAssert(surface.grabberHandle.frame.width == surface.grabberHandleWidth)
|
||||
XCTAssert(surface.grabberHandle.frame.height == surface.grabberHandleHeight)
|
||||
|
||||
surface.grabberHandleWidth = 44.0
|
||||
surface.grabberHandleHeight = 12.0
|
||||
surface.setNeedsLayout()
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssert(surface.grabberHandle.frame.width == surface.grabberHandleWidth, "\(surface.grabberHandle.frame.width) == \(surface.grabberHandleWidth)")
|
||||
XCTAssert(surface.grabberHandle.frame.height == surface.grabberHandleHeight, "\(surface.grabberHandle.frame.height) == \(surface.grabberHandleHeight)")
|
||||
}
|
||||
|
||||
func test_surfaceView_containerMargins() {
|
||||
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssertEqual(surface.containerView.frame, surface.bounds)
|
||||
surface.containerMargins = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
|
||||
surface.setNeedsLayout()
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssertEqual(surface.containerView.frame, surface.bounds.inset(by: surface.containerMargins))
|
||||
}
|
||||
|
||||
func test_surfaceView_contentInsets() {
|
||||
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
|
||||
let contentView = UIView()
|
||||
surface.add(contentView: contentView)
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssertEqual(surface.contentView.frame, surface.bounds)
|
||||
surface.contentInsets = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
|
||||
surface.setNeedsLayout()
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssertEqual(surface.contentView.frame, surface.bounds.inset(by: surface.contentInsets))
|
||||
}
|
||||
|
||||
func test_surfaceView_containerMargins_and_contentInsets() {
|
||||
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
|
||||
let contentView = UIView()
|
||||
surface.add(contentView: contentView)
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssertEqual(surface.contentView.frame, surface.bounds)
|
||||
surface.containerMargins = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
|
||||
surface.contentInsets = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
|
||||
surface.setNeedsLayout()
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssertEqual(surface.containerView.frame, surface.bounds.inset(by: surface.containerMargins))
|
||||
XCTAssertEqual(surface.contentView.frame, surface.containerView.bounds.inset(by: surface.contentInsets))
|
||||
}
|
||||
|
||||
func test_surfaceView_cornderRaduis() {
|
||||
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
|
||||
XCTAssert(surface.cornerRadius == 0.0)
|
||||
XCTAssert(surface.containerView.layer.masksToBounds == false)
|
||||
|
||||
surface.cornerRadius = 10.0
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssert(surface.cornerRadius == 10.0)
|
||||
XCTAssert(surface.containerView.layer.cornerRadius == 10.0)
|
||||
XCTAssert(surface.containerView.layer.masksToBounds == true)
|
||||
|
||||
surface.containerView.layer.cornerRadius = 12.0
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssert(surface.cornerRadius == 12.0)
|
||||
XCTAssert(surface.containerView.layer.masksToBounds == true)
|
||||
|
||||
surface.cornerRadius = 0.0
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssert(surface.cornerRadius == 0.0)
|
||||
XCTAssert(surface.containerView.layer.cornerRadius == 0.0)
|
||||
XCTAssert(surface.containerView.layer.masksToBounds == false)
|
||||
|
||||
surface.containerView.layer.cornerRadius = 12.0
|
||||
surface.setNeedsLayout()
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssert(surface.cornerRadius == 12.0)
|
||||
XCTAssert(surface.containerView.layer.masksToBounds == true)
|
||||
}
|
||||
|
||||
func test_surfaceView_border() {
|
||||
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
|
||||
XCTAssert(surface.borderColor == nil)
|
||||
XCTAssert(surface.borderWidth == 0.0)
|
||||
|
||||
surface.borderColor = .red
|
||||
surface.borderWidth = 3.0
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssert(surface.containerView.layer.borderColor == UIColor.red.cgColor)
|
||||
XCTAssert(surface.containerView.layer.borderWidth == 3.0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,537 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2019/05/23.
|
||||
// Copyright © 2019 Shin Yamamoto. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import FloatingPanel
|
||||
|
||||
class FloatingPanelTests: XCTestCase {
|
||||
override func setUp() {}
|
||||
override func tearDown() {}
|
||||
|
||||
func test_scrolllock() {
|
||||
let fpc = FloatingPanelController()
|
||||
|
||||
let contentVC1 = UITableViewController(nibName: nil, bundle: nil)
|
||||
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, true)
|
||||
XCTAssertEqual(contentVC1.tableView.bounces, true)
|
||||
fpc.set(contentViewController: contentVC1)
|
||||
fpc.track(scrollView: contentVC1.tableView)
|
||||
fpc.showForTest()
|
||||
|
||||
XCTAssertEqual(fpc.position, .half)
|
||||
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, false)
|
||||
XCTAssertEqual(contentVC1.tableView.bounces, false)
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, true)
|
||||
XCTAssertEqual(contentVC1.tableView.bounces, true)
|
||||
|
||||
fpc.move(to: .tip, animated: false)
|
||||
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, false)
|
||||
XCTAssertEqual(contentVC1.tableView.bounces, false)
|
||||
|
||||
let exp1 = expectation(description: "move to full with animation")
|
||||
fpc.move(to: .full, animated: true) {
|
||||
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, true)
|
||||
XCTAssertEqual(contentVC1.tableView.bounces, true)
|
||||
exp1.fulfill()
|
||||
}
|
||||
wait(for: [exp1], timeout: 1.0)
|
||||
|
||||
let exp2 = expectation(description: "move to tip with animation")
|
||||
fpc.move(to: .tip, animated: false) {
|
||||
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, false)
|
||||
XCTAssertEqual(contentVC1.tableView.bounces, false)
|
||||
exp2.fulfill()
|
||||
}
|
||||
wait(for: [exp2], timeout: 1.0)
|
||||
|
||||
// Reset the content vc
|
||||
let contentVC2 = UITableViewController(nibName: nil, bundle: nil)
|
||||
XCTAssertEqual(contentVC2.tableView.showsVerticalScrollIndicator, true)
|
||||
XCTAssertEqual(contentVC2.tableView.bounces, true)
|
||||
fpc.set(contentViewController: contentVC2)
|
||||
fpc.track(scrollView: contentVC2.tableView)
|
||||
fpc.show(animated: false, completion: nil)
|
||||
XCTAssertEqual(fpc.position, .half)
|
||||
XCTAssertEqual(contentVC2.tableView.showsVerticalScrollIndicator, false)
|
||||
XCTAssertEqual(contentVC2.tableView.bounces, false)
|
||||
}
|
||||
|
||||
func test_getBackdropAlpha_1positions() {
|
||||
class FloatingPanelLayout1Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .full
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.full]
|
||||
}
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout1Positions()
|
||||
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos - 100.0, with: CGPoint(x: 0.0, y: -100.0)), 0.3)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: 0.0)), 0.3)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + 100.0, with: CGPoint(x: 0.0, y: 100.0)), 0.3) // ok??
|
||||
}
|
||||
|
||||
func test_getBackdropAlpha_2positions() {
|
||||
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .half
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.half, .full]
|
||||
}
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout2Positions()
|
||||
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let halfPos = fpc.originYOfSurface(for: .half)
|
||||
let distance1 = abs(halfPos - fullPos)
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: 0.0)), 0.3)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + distance1 * 0.5, with: CGPoint(x: 0.0, y: distance1 * 0.5)), 0.3 * 0.5)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: CGPoint(x: 0.0, y: distance1)), 0.0)
|
||||
|
||||
fpc.move(to: .half, animated: false)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: CGPoint(x: 0.0, y: 0.0)), 0.0)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + distance1 * 0.5, with: CGPoint(x: 0.0, y: -0.5 * distance1)), 0.3 * 0.5)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: -1 * distance1)), 0.3)
|
||||
}
|
||||
|
||||
func test_getBackdropAlpha_2positionsWithHidden() {
|
||||
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .hidden
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .full]
|
||||
}
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout2Positions()
|
||||
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let hiddenPos = fpc.originYOfSurface(for: .hidden)
|
||||
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos - 100.0, with: CGPoint(x: 0.0, y: -100.0)), 0.3)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: 0.0)), 0.3)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: hiddenPos, with: CGPoint(x: 0.0, y: 100.0)), 0.0)
|
||||
}
|
||||
|
||||
func test_getBackdropAlpha_3positions() {
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.showForTest()
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let halfPos = fpc.originYOfSurface(for: .half)
|
||||
let tipPos = fpc.originYOfSurface(for: .tip)
|
||||
let distance1 = abs(halfPos - fullPos)
|
||||
let distance2 = abs(tipPos - halfPos)
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: 0.0)), 0.3)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + distance1 * 0.5, with: CGPoint(x: 0.0, y: distance1 * 0.5)), 0.3 * 0.5)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: CGPoint(x: 0.0, y: distance1)), 0.0)
|
||||
|
||||
fpc.move(to: .half, animated: false)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: CGPoint(x: 0.0, y: 0.0)), 0.0)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + distance1 * 0.5, with: CGPoint(x: 0.0, y: -0.5 * distance1)), 0.3 * 0.5)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: -1 * distance1)), 0.3)
|
||||
|
||||
fpc.move(to: .tip, animated: false)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: tipPos, with: CGPoint(x: 0.0, y: 0.0)), 0.0)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos + distance2 * 0.5, with: CGPoint(x: 0.0, y: -0.5 * distance2)), 0.0)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: CGPoint(x: 0.0, y: -1 * distance2)), 0.0)
|
||||
}
|
||||
|
||||
func test_targetPosition_1positions() {
|
||||
class FloatingPanelLayout1Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .full
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.full]
|
||||
}
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout1Positions()
|
||||
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .full), // redirect
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full), // redirect
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full), // redirect
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .full), // redirect
|
||||
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
|
||||
])
|
||||
}
|
||||
|
||||
func test_targetPosition_2positions() {
|
||||
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .half
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.half, .full]
|
||||
}
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout2Positions()
|
||||
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let halfPos = fpc.originYOfSurface(for: .half)
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
|
||||
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
|
||||
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .half), // redirect
|
||||
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
|
||||
])
|
||||
fpc.move(to: .half, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
|
||||
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
|
||||
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .half), // redirect
|
||||
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
|
||||
])
|
||||
}
|
||||
|
||||
func test_targetPosition_2positionsWithHidden() {
|
||||
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .hidden
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .full]
|
||||
}
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout2Positions()
|
||||
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let hiddenPos = fpc.originYOfSurface(for: .hidden)
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
|
||||
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
|
||||
(#line, hiddenPos - 10.0, CGPoint(x: 0.0, y: -100.0), .hidden), // redirect
|
||||
(#line, hiddenPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
|
||||
(#line, hiddenPos, CGPoint(x: 0.0, y: -100.0), .hidden),
|
||||
(#line, hiddenPos, CGPoint(x: 0.0, y: 0.0), .hidden),
|
||||
(#line, hiddenPos, CGPoint(x: 0.0, y: 100.0), .hidden),
|
||||
(#line, hiddenPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // redirect
|
||||
(#line, hiddenPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
|
||||
])
|
||||
fpc.move(to: .hidden, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
|
||||
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
|
||||
(#line, hiddenPos - 10.0, CGPoint(x: 0.0, y: -100.0), .hidden), // redirect
|
||||
(#line, hiddenPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
|
||||
(#line, hiddenPos, CGPoint(x: 0.0, y: -100.0), .hidden),
|
||||
(#line, hiddenPos, CGPoint(x: 0.0, y: 0.0), .hidden),
|
||||
(#line, hiddenPos, CGPoint(x: 0.0, y: 100.0), .hidden),
|
||||
(#line, hiddenPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // redirect
|
||||
(#line, hiddenPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
|
||||
])
|
||||
}
|
||||
|
||||
func test_targetPosition_2positionsFromFull() {
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout3Positions()
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let halfPos = fpc.originYOfSurface(for: .half)
|
||||
let tipPos = fpc.originYOfSurface(for: .tip)
|
||||
// From .full
|
||||
fpc.move(to: .full, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from topMostState
|
||||
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from topMostState
|
||||
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from topMostState
|
||||
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to tip at half
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
|
||||
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
|
||||
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full), //project to full
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip), // project to tip
|
||||
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
|
||||
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirect
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to full at half
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 100.0), .tip),
|
||||
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
|
||||
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from bottomMostState
|
||||
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from bottomMostState
|
||||
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from bottomMostState
|
||||
])
|
||||
}
|
||||
|
||||
func test_targetPosition_3positionsFromHalf() {
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout3Positions()
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let halfPos = fpc.originYOfSurface(for: .half)
|
||||
let tipPos = fpc.originYOfSurface(for: .tip)
|
||||
// From .half
|
||||
fpc.move(to: .half, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from topMostState
|
||||
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from topMostState
|
||||
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from topMostState
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to tip at half
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
|
||||
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
|
||||
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full),// project to full
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip), // project to tip
|
||||
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
|
||||
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirect
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to full at half
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 100.0), .tip),
|
||||
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from bottomMostState
|
||||
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from bottomMostState
|
||||
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from bottomMostState
|
||||
])
|
||||
}
|
||||
|
||||
func test_targetPosition_3positionsFromTip() {
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout3Positions()
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let halfPos = fpc.originYOfSurface(for: .half)
|
||||
let tipPos = fpc.originYOfSurface(for: .tip)
|
||||
|
||||
// From .tip
|
||||
fpc.move(to: .tip, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from topMostState
|
||||
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from topMostState
|
||||
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from topMostState
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to tip at half
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
|
||||
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
|
||||
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -3000.0), .full), // project to full
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip), // project to tip
|
||||
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
|
||||
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirect
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to full at half
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 100.0), .tip),
|
||||
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from bottomMostState
|
||||
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from bottomMostState
|
||||
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from bottomMostState
|
||||
])
|
||||
}
|
||||
|
||||
func test_targetPosition_3positionsAllProjection() {
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout3Positions()
|
||||
delegate.behavior = FloatingPanelProjectionalBehavior()
|
||||
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let halfPos = fpc.originYOfSurface(for: .half)
|
||||
let tipPos = fpc.originYOfSurface(for: .tip)
|
||||
|
||||
// From .full
|
||||
fpc.move(to: .full, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .tip),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .tip),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .tip),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .full),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .full),
|
||||
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: -3000.0), .full),
|
||||
])
|
||||
|
||||
// From .half
|
||||
fpc.move(to: .tip, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .tip),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .full),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .full),
|
||||
])
|
||||
|
||||
// From .tip
|
||||
fpc.move(to: .tip, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .tip),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .tip),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .tip),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .full),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .full),
|
||||
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: -3000.0), .full),
|
||||
])
|
||||
}
|
||||
|
||||
func test_targetPosition_3positionsWithHidden() {
|
||||
class FloatingPanelLayout3PositionsWithHidden: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .hidden
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .half, .full]
|
||||
}
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout3PositionsWithHidden()
|
||||
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
XCTAssertEqual(fpc.position, .hidden)
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: 1000.0), .half),
|
||||
])
|
||||
fpc.move(to: .half, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: -100.0), .half),
|
||||
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: -1000.0), .full),
|
||||
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: 0.0), .half),
|
||||
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: 1000.0), .hidden),
|
||||
])
|
||||
}
|
||||
|
||||
func test_targetPosition_3positionsWithHiddenWithoutFull() {
|
||||
class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .hidden
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .tip, .half]
|
||||
}
|
||||
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout3Positions()
|
||||
delegate.behavior = FloatingPanelProjectionalBehavior()
|
||||
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
XCTAssertEqual(fpc.position, .hidden)
|
||||
|
||||
let halfPos = fpc.originYOfSurface(for: .half)
|
||||
let tipPos = fpc.originYOfSurface(for: .tip)
|
||||
//let hiddenPos = fpc.originYOfSurface(for: .hidden)
|
||||
|
||||
fpc.move(to: .half, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 385.0), .tip), // projection
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // projection
|
||||
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirection
|
||||
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirection
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), //projection
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -10.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 10.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .hidden), //projection
|
||||
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: 10.0), .tip), // redirection
|
||||
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: 10.0), .tip), // redirection
|
||||
])
|
||||
fpc.move(to: .tip, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .hidden),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
private class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .tip
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.tip, .half, .full]
|
||||
}
|
||||
|
||||
private typealias TestParameter = (UInt, CGFloat,CGPoint, FloatingPanelPosition)
|
||||
private func assertTargetPosition(_ floatingPanel: FloatingPanelCore, with params: [TestParameter]) {
|
||||
params.forEach { (line, pos, velocity, result) in
|
||||
floatingPanel.surfaceView.frame.origin.y = pos
|
||||
XCTAssertEqual(floatingPanel.targetPosition(from: pos, with: velocity), result, line: line)
|
||||
}
|
||||
}
|
||||
|
||||
private class FloatingPanelProjectionalBehavior: FloatingPanelBehavior {
|
||||
func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2019/06/27.
|
||||
// Copyright © 2019 scenee. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@testable import FloatingPanel
|
||||
|
||||
func waitRunLoop(secs: TimeInterval = 0) {
|
||||
RunLoop.main.run(until: Date(timeIntervalSinceNow: secs))
|
||||
}
|
||||
|
||||
extension FloatingPanelController {
|
||||
func showForTest() {
|
||||
loadViewIfNeeded()
|
||||
view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
|
||||
show(animated: false, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingPanelTestDelegate: FloatingPanelControllerDelegate {
|
||||
var layout: FloatingPanelLayout?
|
||||
var behavior: FloatingPanelBehavior?
|
||||
var position: FloatingPanelPosition = .hidden
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
|
||||
return layout
|
||||
}
|
||||
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
|
||||
return behavior
|
||||
}
|
||||
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
|
||||
position = vc.position
|
||||
}
|
||||
}
|
||||
|
||||
protocol FloatingPanelTestLayout: FloatingPanelFullScreenLayout {}
|
||||
extension FloatingPanelTestLayout {
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
|
||||
switch position {
|
||||
case .full: return 20.0
|
||||
case .half: return 250.0
|
||||
case .tip: return 60.0
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2018/09/18.
|
||||
// Copyright © 2018 scenee. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import FloatingPanelController
|
||||
|
||||
class ViewTests: XCTestCase {
|
||||
|
||||
override func setUp() {}
|
||||
|
||||
override func tearDown() {}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// swift-tools-version:5.0
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "FloatingPanel",
|
||||
platforms: [
|
||||
.iOS(.v10)
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries produced by a package, and make them visible to other packages.
|
||||
.library(
|
||||
name: "FloatingPanel",
|
||||
targets: ["FloatingPanel"]),
|
||||
],
|
||||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
// .package(url: /* package url */, from: "1.0.0"),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
|
||||
.target(name: "FloatingPanel", path: "Framework/Sources"),
|
||||
],
|
||||
swiftLanguageVersions: [.version("5")]
|
||||
)
|
||||
@@ -1,4 +1,13 @@
|
||||
# FloatingPanel
|
||||
[](https://travis-ci.org/SCENEE/FloatingPanel)
|
||||
[](https://cocoapods.org/pods/FloatingPanel)
|
||||
[](https://github.com/Carthage/Carthage)
|
||||
[](https://cocoapods.org/pods/FloatingPanel)
|
||||
[](https://swift.org/)
|
||||
[](https://swift.org/)
|
||||
[](https://swift.org/)
|
||||
[](https://swift.org/)
|
||||
|
||||
# FloatingPanel
|
||||
|
||||
FloatingPanel is a simple and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
|
||||
The new interface displays the related contents and utilities in parallel as a user wants.
|
||||
@@ -6,6 +15,51 @@ The new interface displays the related contents and utilities in parallel as a u
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
- [Features](#features)
|
||||
- [Requirements](#requirements)
|
||||
- [Installation](#installation)
|
||||
- [CocoaPods](#cocoapods)
|
||||
- [Carthage](#carthage)
|
||||
- [Swift Package Manager with Xcode 11](#swift-package-manager-with-xcode-11)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Add a floating panel as a child view controller](#add-a-floating-panel-as-a-child-view-controller)
|
||||
- [Present a floating panel as a modality](#present-a-floating-panel-as-a-modality)
|
||||
- [View hierarchy](#view-hierarchy)
|
||||
- [Usage](#usage)
|
||||
- [Show/Hide a floating panel in a view with your view hierarchy](#showhide-a-floating-panel-in-a-view-with-your-view-hierarchy)
|
||||
- [Scale the content view when the surface position changes](#scale-the-content-view-when-the-surface-position-changes)
|
||||
- [Customize the layout with `FloatingPanelLayout` protocol](#customize-the-layout-with-floatingpanellayout-protocol)
|
||||
- [Change the initial position and height](#change-the-initial-position-and-height)
|
||||
- [Support your landscape layout](#support-your-landscape-layout)
|
||||
- [Use Intrinsic height layout](#use-intrinsic-height-layout)
|
||||
- [Specify position insets from the frame of `FloatingPanelContrller.view`, not the SafeArea](#specify-position-insets-from-the-frame-of-floatingpanelcontrllerview-not-the-safearea)
|
||||
- [Customize the behavior with `FloatingPanelBehavior` protocol](#customize-the-behavior-with-floatingpanelbehavior-protocol)
|
||||
- [Modify your floating panel's interaction](#modify-your-floating-panels-interaction)
|
||||
- [Activate the rubber-band effect on the top/bottom edges](#activate-the-rubber-band-effect-on-the-topbottom-edges)
|
||||
- [Manage the projection of a pan gesture momentum](#manage-the-projection-of-a-pan-gesture-momentum)
|
||||
- [Customize the surface design](#customize-the-surface-design)
|
||||
- [Use a custom grabber handle](#use-a-custom-grabber-handle)
|
||||
- [Customize layout of the grabber handle](#customize-layout-of-the-grabber-handle)
|
||||
- [Customize content padding from surface edges](#customize-content-padding-from-surface-edges)
|
||||
- [Customize margins of the surface edges](#customize-margins-of-the-surface-edges)
|
||||
- [Customize gestures](#customize-gestures)
|
||||
- [Suppress the panel interaction](#suppress-the-panel-interaction)
|
||||
- [Add tap gestures to the surface or backdrop views](#add-tap-gestures-to-the-surface-or-backdrop-views)
|
||||
- [Create an additional floating panel for a detail](#create-an-additional-floating-panel-for-a-detail)
|
||||
- [Move a position with an animation](#move-a-position-with-an-animation)
|
||||
- [Work your contents together with a floating panel behavior](#work-your-contents-together-with-a-floating-panel-behavior)
|
||||
- [Notes](#notes)
|
||||
- ['Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller](#show-or-show-detail-segues-from-floatingpanelcontrollers-content-view-controller)
|
||||
- [UISearchController issue](#uisearchcontroller-issue)
|
||||
- [FloatingPanelSurfaceView's issue on iOS 10](#floatingpanelsurfaceviews-issue-on-ios-10)
|
||||
- [Author](#author)
|
||||
- [License](#license)
|
||||
|
||||
<!-- /TOC -->
|
||||
|
||||
## Features
|
||||
|
||||
@@ -13,10 +67,11 @@ The new interface displays the related contents and utilities in parallel as a u
|
||||
- [x] Fluid animation and gesture handling
|
||||
- [x] Scroll view tracking
|
||||
- [x] Common UI elements: Grabber handle, Backdrop and Surface rounding corners
|
||||
- [x] 2 or 3 anchor positions(full, half, tip)
|
||||
- [x] 1~3 anchor positions(full, half, tip)
|
||||
- [x] Layout customization for all trait environments(i.e. Landscape orientation support)
|
||||
- [x] Behavior customization
|
||||
- [x] Free from common issues of Auto Layout and gesture handling
|
||||
- [x] Modal presentation
|
||||
|
||||
Examples are here.
|
||||
|
||||
@@ -25,7 +80,9 @@ Examples are here.
|
||||
|
||||
## Requirements
|
||||
|
||||
FloatingPanel is written in Swift 4.2. Compatible with iOS 10.0+
|
||||
FloatingPanel is written in Swift 4.0+. It can be built by Xcode 9.4.1 or later. Compatible with iOS 10.0+.
|
||||
|
||||
✏️ The default Swift version is 4.0 because it avoids build errors with Carthage on each Xcode version from the source compatibility between Swift 4.0, 4.2 and 5.0.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -38,6 +95,8 @@ it, simply add the following line to your Podfile:
|
||||
pod 'FloatingPanel'
|
||||
```
|
||||
|
||||
✏️FloatingPanel v1.7.0 or later requires CocoaPods v1.7.0+ for `swift_versions` support.
|
||||
|
||||
### Carthage
|
||||
|
||||
For [Carthage](https://github.com/Carthage/Carthage), add the following to your `Cartfile`:
|
||||
@@ -46,9 +105,14 @@ For [Carthage](https://github.com/Carthage/Carthage), add the following to your
|
||||
github "scenee/FloatingPanel"
|
||||
```
|
||||
|
||||
### Swift Package Manager with Xcode 11
|
||||
|
||||
Follow [this doc](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Add a floating panel as a child view controller
|
||||
|
||||
```swift
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
@@ -58,49 +122,403 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
// Initialize FloatingPanelController and add the view
|
||||
// Initialize a `FloatingPanelController` object.
|
||||
fpc = FloatingPanelController()
|
||||
fpc.delegate = self
|
||||
|
||||
// Add a content view controller
|
||||
// Assign self as the delegate of the controller.
|
||||
fpc.delegate = self // Optional
|
||||
|
||||
// Set a content view controller.
|
||||
let contentVC = ContentViewController()
|
||||
fpc.show(contentVC, sender: nil)
|
||||
fpc.set(contentViewController: contentVC)
|
||||
|
||||
// Track a scroll view in the Content VC.
|
||||
// Track a scroll view(or the siblings) in the content view controller.
|
||||
fpc.track(scrollView: contentVC.tableView)
|
||||
|
||||
// Add FloatingPanel to self.view
|
||||
fpc.add(toParent: self)
|
||||
// Add and show the views managed by the `FloatingPanelController` object to self.view.
|
||||
fpc.addPanel(toParent: self)
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
// Remove FloatingPanel from self.view
|
||||
fpc.removeFromParent()
|
||||
|
||||
// Remove the views managed by the `FloatingPanelController` object from self.view.
|
||||
fpc.removePanelFromParent()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Present a floating panel as a modality
|
||||
|
||||
```swift
|
||||
let fpc = FloatingPanelController()
|
||||
let contentVC = ...
|
||||
fpc.set(contentViewController: contentVC)
|
||||
|
||||
fpc.isRemovalInteractionEnabled = true // Optional: Let it removable by a swipe-down
|
||||
|
||||
self.present(fpc, animated: true, completion: nil)
|
||||
```
|
||||
|
||||
You can show a floating panel over UINavigationController from the container view controllers as a modality of `.overCurrentContext` style.
|
||||
|
||||
✏️ FloatingPanelController has the custom presentation controller. If you would like to customize the presentation/dismissal, please see [FloatingPanelTransitioning](https://github.com/SCENEE/FloatingPanel/blob/master/Framework/Sources/FloatingPanelTransitioning.swift).
|
||||
|
||||
## View hierarchy
|
||||
|
||||
`FloatingPanelController` manages the views as the following view hierarchy.
|
||||
|
||||
```
|
||||
FloatingPanelController.view (FloatingPanelPassThroughView)
|
||||
├─ .backdropView (FloatingPanelBackdropView)
|
||||
└─ .surfaceView (FloatingPanelSurfaceView)
|
||||
├─ .containerView (UIView)
|
||||
│ └─ .contentView (FloatingPanelController.contentViewController.view)
|
||||
└─ .grabberHandle (GrabberHandleView)
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Show/Hide a floating panel in a view with your view hierarchy
|
||||
|
||||
If you need more control over showing and hiding the floating panel, you can forgo the `addPanel` and `removePanelFromParent` methods. These methods are a convenience wrapper for **FloatingPanel**'s `show` and `hide` methods along with some required setup.
|
||||
|
||||
There are two ways to work with the `FloatingPanelController`:
|
||||
1. Add it to the hierarchy once and then call `show` and `hide` methods to make it appear/disappear.
|
||||
2. Add it to the hierarchy when needed and remove afterwards.
|
||||
|
||||
The following example shows how to add the controller to your `UIViewController` and how to remove it. Make sure that you never add the same `FloatingPanelController` to the hierarchy before removing it.
|
||||
|
||||
**NOTE**: `self.` prefix is not required, nor recommended. It's used here to make it clearer where do the functions used come from. `self` is an instance of a custom UIViewController in your code.
|
||||
|
||||
```swift
|
||||
// Add the floating panel view to the controller's view on top of other views.
|
||||
self.view.addSubview(fpc.view)
|
||||
|
||||
// REQUIRED. It makes the floating panel view have the same size as the controller's view.
|
||||
fpc.view.frame = self.view.bounds
|
||||
|
||||
// In addition, Auto Layout constraints are highly recommended.
|
||||
// Constraint the fpc.view to all four edges of your controller's view.
|
||||
// It makes the layout more robust on trait collection change.
|
||||
fpc.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
fpc.view.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0.0),
|
||||
fpc.view.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 0.0),
|
||||
fpc.view.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: 0.0),
|
||||
fpc.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0.0),
|
||||
])
|
||||
|
||||
// Add the floating panel controller to the controller hierarchy.
|
||||
self.addChild(fpc)
|
||||
|
||||
// Show the floating panel at the initial position defined in your `FloatingPanelLayout` object.
|
||||
fpc.show(animated: true) {
|
||||
// Inform the floating panel controller that the transition to the controller hierarchy has completed.
|
||||
fpc.didMove(toParent: self)
|
||||
}
|
||||
```
|
||||
|
||||
After you add the `FloatingPanelController` as seen above, you can call `fpc.show(animated: true) { }` to show the panel and `fpc.hide(animated: true) { }` to hide it.
|
||||
|
||||
To remove the `FloatingPanelController` from the hierarchy, follow the example below.
|
||||
|
||||
```swift
|
||||
// Inform the panel controller that it will be removed from the hierarchy.
|
||||
fpc.willMove(toParent: nil)
|
||||
|
||||
// Hide the floating panel.
|
||||
fpc.hide(animated: true) {
|
||||
// Remove the floating panel view from your controller's view.
|
||||
fpc.view.removeFromSuperview()
|
||||
// Remove the floating panel controller from the controller hierarchy.
|
||||
fpc.removeFromParent()
|
||||
}
|
||||
```
|
||||
|
||||
### Scale the content view when the surface position changes
|
||||
|
||||
Specify the `contentMode` to `.fitToBounds` if the surface height fits the bounds of `FloatingPanelController.view` when the surface position changes
|
||||
|
||||
```swift
|
||||
fpc.contentMode = .fitToBounds
|
||||
```
|
||||
|
||||
Otherwise, `FloatingPanelController` fixes the content by the height of the top most position.
|
||||
|
||||
✏️ In `.fitToBounds` mode, the surface height changes as following a user interaction so that you have a responsibility to configure Auto Layout constrains not to break the layout of a content view by the elastic surface height.
|
||||
|
||||
### Customize the layout with `FloatingPanelLayout` protocol
|
||||
|
||||
#### Change the initial position and height
|
||||
|
||||
```swift
|
||||
class ViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
...
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
|
||||
return MyFloatingPanelLayout()
|
||||
}
|
||||
}
|
||||
|
||||
class MyFloatingPanelLayout: FloatingPanelLayout {
|
||||
public var initialPosition: FloatingPanelPosition {
|
||||
return .tip
|
||||
}
|
||||
|
||||
public func insetFor(position: FloatingPanelPosition) -> CGFloat? {
|
||||
switch position {
|
||||
case .full: return 16.0 // A top inset from safe area
|
||||
case .half: return 216.0 // A bottom inset from the safe area
|
||||
case .tip: return 44.0 // A bottom inset from the safe area
|
||||
default: return nil // Or `case .hidden: return nil`
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Support your landscape layout
|
||||
|
||||
```swift
|
||||
class ViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
...
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
|
||||
return (newCollection.verticalSizeClass == .compact) ? FloatingPanelLandscapeLayout() : nil // Returning nil indicates to use the default layout
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingPanelLandscapeLayout: FloatingPanelLayout {
|
||||
public var initialPosition: FloatingPanelPosition {
|
||||
return .tip
|
||||
}
|
||||
public var supportedPositions: Set<FloatingPanelPosition> {
|
||||
return [.full, .tip]
|
||||
}
|
||||
|
||||
public func insetFor(position: FloatingPanelPosition) -> CGFloat? {
|
||||
switch position {
|
||||
case .full: return 16.0
|
||||
case .tip: return 69.0
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
|
||||
return [
|
||||
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0),
|
||||
surfaceView.widthAnchor.constraint(equalToConstant: 291),
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Use Intrinsic height layout
|
||||
|
||||
1. Lay out your content View with the intrinsic height size. For example, see "Detail View Controller scene"/"Intrinsic View Controller scene" of [Main.storyboard](https://github.com/SCENEE/FloatingPanel/blob/master/Examples/Samples/Sources/Base.lproj/Main.storyboard). The 'Stack View.bottom' constraint determines the intrinsic height.
|
||||
2. Create a layout that adopts and conforms to `FloatingPanelIntrinsicLayout` and use it.
|
||||
|
||||
```swift
|
||||
class ViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
...
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
|
||||
return RemovablePanelLayout()
|
||||
}
|
||||
}
|
||||
|
||||
class RemovablePanelLayout: FloatingPanelIntrinsicLayout {
|
||||
var supportedPositions: Set<FloatingPanelPosition> {
|
||||
return [.full, .half]
|
||||
}
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
|
||||
switch position {
|
||||
case .half: return 130.0
|
||||
default: return nil // Must return nil for .full
|
||||
}
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
#### Specify position insets from the frame of `FloatingPanelContrller.view`, not the SafeArea
|
||||
|
||||
### Move a positon with an animation
|
||||
|
||||
Move a floating panel to the top and middle of a view while opening and closeing a search bar like Apple Maps.
|
||||
There are 2 ways. One is returning `.fromSuperview` for `FloatingPanelLayout.positionReference` in your layout.
|
||||
|
||||
```swift
|
||||
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
||||
...
|
||||
fpc.move(to: .half, animated: true)
|
||||
}
|
||||
|
||||
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
|
||||
...
|
||||
fpc.move(to: .full, animated: true)
|
||||
class MyFullScreenLayout: FloatingPanelLayout {
|
||||
...
|
||||
var positionReference: FloatingPanelLayoutReference {
|
||||
return .fromSuperview
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Make your contents correspond with FloatingPanel behavior
|
||||
Another is using `FloatingPanelFullScreenLayout` protocol.
|
||||
|
||||
```swift
|
||||
class MyFullScreenLayout: FloatingPanelFullScreenLayout {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Customize the behavior with `FloatingPanelBehavior` protocol
|
||||
|
||||
#### Modify your floating panel's interaction
|
||||
|
||||
```swift
|
||||
class ViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
...
|
||||
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
|
||||
return FloatingPanelStocksBehavior()
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingPanelStocksBehavior: FloatingPanelBehavior {
|
||||
...
|
||||
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
|
||||
let damping = self.damping(with: velocity)
|
||||
let springTiming = UISpringTimingParameters(dampingRatio: damping, initialVelocity: velocity)
|
||||
return UIViewPropertyAnimator(duration: 0.5, timingParameters: springTiming)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Activate the rubber-band effect on the top/bottom edges
|
||||
|
||||
```swift
|
||||
class FloatingPanelBehavior: FloatingPanelBehavior {
|
||||
...
|
||||
func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Manage the projection of a pan gesture momentum
|
||||
|
||||
This allows full projectional panel behavior. For example, a user can swipe up a panel from tip to full nearby the tip position.
|
||||
|
||||
```swift
|
||||
class FloatingPanelBehavior: FloatingPanelBehavior {
|
||||
...
|
||||
func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Customize the surface design
|
||||
|
||||
#### Use a custom grabber handle
|
||||
|
||||
```swift
|
||||
let myGrabberHandleView = MyGrabberHandleView()
|
||||
fpc.surfaceView.grabberHandle.isHidden = true
|
||||
fpc.surfaceView.addSubview(myGrabberHandleView)
|
||||
```
|
||||
|
||||
#### Customize layout of the grabber handle
|
||||
|
||||
```swift
|
||||
fpc.surfaceView.grabberTopPadding = 10.0
|
||||
fpc.surfaceView.grabberHandleWidth = 44.0
|
||||
fpc.surfaceView.grabberHandleHeight = 12.0
|
||||
```
|
||||
|
||||
#### Customize content padding from surface edges
|
||||
|
||||
```swift
|
||||
fpc.surfaceView.contentInsets = .init(top: 20, left: 20, bottom: 20, right: 20)
|
||||
```
|
||||
|
||||
#### Customize margins of the surface edges
|
||||
|
||||
```swift
|
||||
fpc.surfaceView.containerMargins = .init(top: 20.0, left: 16.0, bottom: 16.0, right: 16.0)
|
||||
```
|
||||
|
||||
The feature can be used for these 2 kind panels
|
||||
|
||||
* Facebook/Slack-like panel whose surface top edge is separated from the grabber handle.
|
||||
* iOS native panel to display AirPods information, for example.
|
||||
|
||||
### Customize gestures
|
||||
|
||||
#### Suppress the panel interaction
|
||||
|
||||
You can disable the pan gesture recognizer directly
|
||||
|
||||
```swift
|
||||
fpc.panGestureRecognizer.isEnable = false
|
||||
```
|
||||
|
||||
Or use this `FloatingPanelControllerDelegate` method.
|
||||
|
||||
```swift
|
||||
func floatingPanelShouldBeginDragging(_ vc: FloatingPanelController) -> Bool {
|
||||
return aCondition ? false : true
|
||||
}
|
||||
```
|
||||
|
||||
#### Add tap gestures to the surface or backdrop views
|
||||
|
||||
```swift
|
||||
override func viewDidLoad() {
|
||||
...
|
||||
surfaceTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleSurface(tapGesture:)))
|
||||
fpc.surfaceView.addGestureRecognizer(surfaceTapGesture)
|
||||
|
||||
backdropTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
|
||||
fpc.backdropView.addGestureRecognizer(backdropTapGesture)
|
||||
|
||||
surfaceTapGesture.isEnabled = (fpc.position == .tip)
|
||||
}
|
||||
|
||||
// Enable `surfaceTapGesture` only at `tip` position
|
||||
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
|
||||
surfaceTapGesture.isEnabled = (vc.position == .tip)
|
||||
}
|
||||
```
|
||||
|
||||
### Create an additional floating panel for a detail
|
||||
|
||||
```swift
|
||||
override func viewDidLoad() {
|
||||
// Setup Search panel
|
||||
self.searchPanelVC = FloatingPanelController()
|
||||
|
||||
let searchVC = SearchViewController()
|
||||
self.searchPanelVC.set(contentViewController: searchVC)
|
||||
self.searchPanelVC.track(scrollView: contentVC.tableView)
|
||||
|
||||
self.searchPanelVC.addPanel(toParent: self)
|
||||
|
||||
// Setup Detail panel
|
||||
self.detailPanelVC = FloatingPanelController()
|
||||
|
||||
let contentVC = ContentViewController()
|
||||
self.detailPanelVC.set(contentViewController: contentVC)
|
||||
self.detailPanelVC.track(scrollView: contentVC.scrollView)
|
||||
|
||||
self.detailPanelVC.addPanel(toParent: self)
|
||||
}
|
||||
```
|
||||
|
||||
### Move a position with an animation
|
||||
|
||||
In the following example, I move a floating panel to full or half position while opening or closing a search bar like Apple Maps.
|
||||
|
||||
```swift
|
||||
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
||||
...
|
||||
fpc.move(to: .half, animated: true)
|
||||
}
|
||||
|
||||
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
|
||||
...
|
||||
fpc.move(to: .full, animated: true)
|
||||
}
|
||||
```
|
||||
|
||||
### Work your contents together with a floating panel behavior
|
||||
|
||||
```swift
|
||||
class ViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
@@ -117,105 +535,67 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
searchVC.hideHeader()
|
||||
}
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Support your landscape layout with a `FloatingPanelLayout` object
|
||||
## Notes
|
||||
|
||||
### 'Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller
|
||||
|
||||
'Show' or 'Show Detail' segues from a content view controller will be managed by a view controller(hereinafter called 'master VC') adding a floating panel. Because a floating panel is just a subview of the master VC(except for modality).
|
||||
|
||||
`FloatingPanelController` has no way to manage a stack of view controllers like `UINavigationController`. If so, it would be so complicated and the interface will become `UINavigationController`. This component should not have the responsibility to manage the stack.
|
||||
|
||||
By the way, a content view controller can present a view controller modally with `present(_:animated:completion:)` or 'Present Modally' segue.
|
||||
|
||||
However, sometimes you want to show a destination view controller of 'Show' or 'Show Detail' segue with another floating panel. It's possible to override `show(_:sender)` of the master VC!
|
||||
|
||||
Here is an example.
|
||||
|
||||
```swift
|
||||
class ViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
class ViewController: UIViewController {
|
||||
var fpc: FloatingPanelController!
|
||||
var secondFpc: FloatingPanelController!
|
||||
|
||||
...
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
|
||||
return (newCollection.verticalSizeClass == .compact) ? FloatingPanelLandscapeLayout() : nil
|
||||
}
|
||||
...
|
||||
}
|
||||
override func show(_ vc: UIViewController, sender: Any?) {
|
||||
secondFpc = FloatingPanelController()
|
||||
|
||||
class FloatingPanelLandscapeLayout: FloatingPanelLayout {
|
||||
public var initialPosition: FloatingPanelPosition {
|
||||
return .tip
|
||||
}
|
||||
public var supportedPositions: [FloatingPanelPosition] {
|
||||
return [.full, .tip]
|
||||
}
|
||||
secondFpc.set(contentViewController: vc)
|
||||
|
||||
public func insetFor(position: FloatingPanelPosition) -> CGFloat? {
|
||||
switch position {
|
||||
case .full: return 16.0
|
||||
case .tip: return 69.0
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
|
||||
return [
|
||||
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuid.leftAnchor, constant: 8.0),
|
||||
surfaceView.widthAnchor.constraint(equalToConstant: 291),
|
||||
]
|
||||
secondFpc.addPanel(toParent: self)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Modify your floating panel's interaction with a `FloatingPanelBehavior` object
|
||||
A `FloatingPanelController` object proxies an action for `show(_:sender)` to the master VC. That's why the master VC can handle a destination view controller of a 'Show' or 'Show Detail' segue and you can hook `show(_:sender)` to show a secondary floating panel set the destination view controller to the content.
|
||||
|
||||
It's a great way to decouple between a floating panel and the content VC.
|
||||
|
||||
### UISearchController issue
|
||||
|
||||
`UISearchController` isn't able to be used with `FloatingPanelController` by the system design.
|
||||
|
||||
Because `UISearchController` automatically presents itself modally when a user interacts with the search bar, and then it swaps the superview of the search bar to the view managed by itself while it displays. As a result, `FloatingPanelController` can't control the search bar when it's active, as you can see from [the screen shot](https://github.com/SCENEE/FloatingPanel/issues/248#issuecomment-521263831).
|
||||
|
||||
### FloatingPanelSurfaceView's issue on iOS 10
|
||||
|
||||
* On iOS 10, `FloatingPanelSurfaceView.cornerRadius` isn't not automatically masked with the top rounded corners because of `UIVisualEffectView` issue. See https://forums.developer.apple.com/thread/50854.
|
||||
So you need to draw top rounding corners of your content. Here is an example in Examples/Maps.
|
||||
```swift
|
||||
class ViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
...
|
||||
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
|
||||
return FloatingPanelStocksBehavior()
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
if #available(iOS 10, *) {
|
||||
visualEffectView.layer.cornerRadius = 9.0
|
||||
visualEffectView.clipsToBounds = true
|
||||
}
|
||||
...
|
||||
}
|
||||
...
|
||||
|
||||
class FloatingPanelStocksBehavior: FloatingPanelBehavior {
|
||||
var velocityThreshold: CGFloat {
|
||||
return 15.0
|
||||
}
|
||||
|
||||
func interactionAnimator(to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
|
||||
let damping = self.damping(with: velocity)
|
||||
let springTiming = UISpringTimingParameters(dampingRatio: damping, initialVelocity: velocity)
|
||||
return UIViewPropertyAnimator(duration: 0.5, timingParameters: springTiming)
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Create an additional floating panel for a detail
|
||||
|
||||
```swift
|
||||
class ViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
var searchPanelVC: FloatingPanelController!
|
||||
var detailPanelVC: FloatingPanelController!
|
||||
|
||||
override func viewDidLoad() {
|
||||
// Setup Search panel
|
||||
self.searchPanelVC = FloatingPanelController()
|
||||
|
||||
let searchVC = SearchViewController()
|
||||
self.searchPanelVC.show(searchVC, sender: nil)
|
||||
self.searchPanelVC.track(scrollView: contentVC.tableView)
|
||||
|
||||
self.searchPanelVC.add(toParent: self)
|
||||
|
||||
// Setup Detail panel
|
||||
self.detailPanelVC = FloatingPanelController()
|
||||
|
||||
let contentVC = ContentViewController()
|
||||
self.detailPanelVC.show(contentVC, sender: nil)
|
||||
self.detailPanelVC.track(scrollView: contentVC.scrollView)
|
||||
|
||||
self.detailPanelVC.add(toParent: self)
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
* If you sets clear color to `FloatingPanelSurfaceView.backgroundColor`, please note the bottom overflow of your content on bouncing at full position. To prevent it, you need to expand your content. For example, See Example/Maps App's Auto Layout settings of `UIVisualEffectView` in Main.storyboard.
|
||||
|
||||
## Author
|
||||
|
||||
Shin Yamamoto, shin@scenee.com
|
||||
Shin Yamamoto <shin@scenee.com> | [@scenee](https://twitter.com/scenee)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
Reference in New Issue
Block a user