├── .gitignore
├── CombineUIKit
├── Resources
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Base.lproj
│ │ └── LaunchScreen.storyboard
│ └── Info.plist
├── App
│ ├── SceneDelegate.swift
│ └── AppDelegate.swift
├── Shared
│ ├── Photo.swift
│ └── Requests.swift
└── Scenes
│ └── Search
│ ├── PhotoCell.swift
│ ├── SearchViewModel.swift
│ ├── SearchViewController.swift
│ ├── CustomGridLayout.swift
│ └── Base.lproj
│ └── Search.storyboard
├── CombineUIKit.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
├── xcuserdata
│ └── gregprice.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── project.pbxproj
├── CombineUIKitTests
├── Info.plist
└── CombineUIKitTests.swift
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /CombineUIKit/Resources/Config.xcconfig
2 |
--------------------------------------------------------------------------------
/CombineUIKit/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/CombineUIKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/CombineUIKit/Resources/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/CombineUIKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/CombineUIKit.xcodeproj/xcuserdata/gregprice.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | CombineUIKit.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/CombineUIKit/App/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // CombineUIKit
4 | //
5 | // Created by Greg Price on 30/03/2021.
6 | //
7 |
8 | import UIKit
9 |
10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 |
12 | var window: UIWindow?
13 |
14 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
15 | guard let _ = (scene as? UIWindowScene) else { return }
16 | }
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/CombineUIKit/App/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // CombineUIKit
4 | //
5 | // Created by Greg Price on 30/03/2021.
6 | //
7 |
8 | import UIKit
9 |
10 | @main
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 |
13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
14 | return true
15 | }
16 |
17 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
18 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
19 | }
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/CombineUIKit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "CombineCocoa",
6 | "repositoryURL": "https://github.com/CombineCommunity/CombineCocoa",
7 | "state": {
8 | "branch": null,
9 | "revision": "749c16693b3a6fa1af2b95256fda734e7ec2258b",
10 | "version": "0.2.2"
11 | }
12 | },
13 | {
14 | "package": "CombineDataSources",
15 | "repositoryURL": "https://github.com/CombineCommunity/CombineDataSources",
16 | "state": {
17 | "branch": null,
18 | "revision": "362795e0336f2c1637036f692a3b5d05806302b4",
19 | "version": "0.2.5"
20 | }
21 | }
22 | ]
23 | },
24 | "version": 1
25 | }
26 |
--------------------------------------------------------------------------------
/CombineUIKitTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/CombineUIKit/Shared/Photo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Photo.swift
3 | // CombineUIKit
4 | //
5 | // Created by Greg Price on 30/03/2021.
6 | //
7 |
8 | struct SearchPhotos: Decodable {
9 | let results: [Photo]
10 | }
11 |
12 | extension SearchPhotos {
13 | static var emptyResults: SearchPhotos {
14 | SearchPhotos(results: [])
15 | }
16 | }
17 |
18 | struct Photo: Decodable {
19 | let id: String
20 | let urls: PhotoUrls
21 | }
22 |
23 | struct PhotoUrls: Decodable {
24 | let raw: String
25 | let full: String
26 | let regular: String
27 | let small: String
28 | let thumb: String
29 | }
30 |
31 | extension Photo: Hashable, Equatable {
32 | static func == (lhs: Photo, rhs: Photo) -> Bool {
33 | lhs.id == rhs.id
34 | }
35 | }
36 |
37 | extension PhotoUrls: Hashable, Equatable {
38 | static func == (lhs: PhotoUrls, rhs: PhotoUrls) -> Bool {
39 | lhs.raw == rhs.raw
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/CombineUIKitTests/CombineUIKitTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CombineUIKitTests.swift
3 | // CombineUIKitTests
4 | //
5 | // Created by Greg Price on 30/03/2021.
6 | //
7 |
8 | import XCTest
9 | @testable import CombineUIKit
10 |
11 | class CombineUIKitTests: XCTestCase {
12 |
13 | override func setUpWithError() throws {
14 | // Put setup code here. This method is called before the invocation of each test method in the class.
15 | }
16 |
17 | override func tearDownWithError() throws {
18 | // Put teardown code here. This method is called after the invocation of each test method in the class.
19 | }
20 |
21 | func testExample() throws {
22 | // This is an example of a functional test case.
23 | // Use XCTAssert and related functions to verify your tests produce the correct results.
24 | }
25 |
26 | func testPerformanceExample() throws {
27 | // This is an example of a performance test case.
28 | self.measure {
29 | // Put the code you want to measure the time of here.
30 | }
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/CombineUIKit/Scenes/Search/PhotoCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoCell.swift
3 | // CombineUIKit
4 | //
5 | // Created by Greg Price on 31/03/2021.
6 | //
7 |
8 | import UIKit
9 | import Combine
10 |
11 | final class PhotoCell: UICollectionViewCell {
12 |
13 | @IBOutlet weak var imageView: UIImageView!
14 | @Published private var image: UIImage? = nil
15 |
16 | private var subscriptions = Set()
17 |
18 | override func prepareForReuse() {
19 | super.prepareForReuse()
20 | subscriptions = Set()
21 | }
22 |
23 | func bind(_ photo: Photo) {
24 | guard let url = URL(string: photo.urls.regular) else {
25 | imageView.image = nil
26 | return
27 | }
28 |
29 | URLSession.shared
30 | .dataTaskPublisher(for: url)
31 | .map(\.data)
32 | .map { UIImage(data: $0) }
33 | .replaceError(with: nil)
34 | .receive(on: DispatchQueue.main)
35 | .assign(to: &$image)
36 |
37 | $image
38 | .sink { [weak self] image in
39 | self?.imageView.image = image
40 | }
41 | .store(in: &subscriptions)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/CombineUIKit/Scenes/Search/SearchViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchViewModel.swift
3 | // CombineUIKit
4 | //
5 | // Created by Greg Price on 30/03/2021.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 | final class SearchViewModel {
12 |
13 | @Published var photos: [Photo] = []
14 | @Published var searching: Bool = false
15 |
16 | func bind(searchQuery: AnyPublisher) {
17 |
18 | let search = searchQuery
19 | .debounce(for: .seconds(0.5), scheduler: DispatchQueue.global())
20 | .map { URLRequest.searchPhotos(query: $0) }
21 | .share()
22 |
23 | let photos = search
24 | .map { API.publisher(for: $0) }
25 | .switchToLatest()
26 | .decode(type: SearchPhotos.self, decoder: API.jsonDecoder)
27 | .replaceError(with: .emptyResults)
28 | .share()
29 |
30 | photos
31 | .map(\.results)
32 | .receive(on: DispatchQueue.main)
33 | .assign(to: &$photos)
34 |
35 | search
36 | .map { _ in true }
37 | .merge(with: photos
38 | .map { _ in false }
39 | .delay(for: .seconds(0.5), scheduler: DispatchQueue.global()))
40 | .replaceError(with: false)
41 | .receive(on: DispatchQueue.main)
42 | .assign(to: &$searching)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/CombineUIKit/Scenes/Search/SearchViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchViewController.swift
3 | // CombineUIKit
4 | //
5 | // Created by Greg Price on 30/03/2021.
6 | //
7 |
8 | import UIKit
9 | import Combine
10 | import CombineCocoa
11 | import CombineDataSources
12 |
13 | final class SearchViewController: UIViewController {
14 |
15 | @IBOutlet weak var activityView: UIActivityIndicatorView!
16 | @IBOutlet weak var searchBar: UISearchBar!
17 | @IBOutlet weak var collectionView: UICollectionView!
18 |
19 | private let viewModel = SearchViewModel()
20 | private var subscriptions = Set()
21 |
22 | override func viewDidLoad() {
23 | super.viewDidLoad()
24 |
25 | viewModel.bind(searchQuery: searchBar.textDidChangePublisher)
26 |
27 | viewModel.$photos
28 | .bind(subscriber: collectionView.itemsSubscriber(cellIdentifier: "Cell", cellType: PhotoCell.self, cellConfig: { cell, _, photo in
29 | cell.bind(photo)
30 | }))
31 | .store(in: &subscriptions)
32 |
33 | viewModel.$searching
34 | .sink { [weak activityView] searching in
35 | searching ? activityView?.startAnimating() : activityView?.stopAnimating()
36 | }
37 | .store(in: &subscriptions)
38 |
39 | searchBar.searchButtonClickedPublisher
40 | .sink { [weak searchBar] in
41 | searchBar?.resignFirstResponder()
42 | }
43 | .store(in: &subscriptions)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/CombineUIKit/Resources/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/CombineUIKit/Shared/Requests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Requests.swift
3 | // CombineUIKit
4 | //
5 | // Created by Greg Price on 30/03/2021.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 | enum API {
12 |
13 | static let jsonDecoder: JSONDecoder = {
14 | let decoder = JSONDecoder()
15 | decoder.keyDecodingStrategy = .convertFromSnakeCase
16 | return decoder
17 | }()
18 |
19 | static func publisher(for request: URLRequest) -> AnyPublisher {
20 | URLSession.shared
21 | .dataTaskPublisher(for: request)
22 | .map(\.data)
23 | .eraseToAnyPublisher()
24 | }
25 | }
26 |
27 | extension URLComponents {
28 |
29 | static func unsplash(path: String, queryItems: [String: String]) -> URLComponents {
30 | var components = URLComponents()
31 | components.scheme = "https"
32 | components.host = "api.unsplash.com"
33 | components.path = path
34 | components.queryItems = queryItems.map { URLQueryItem(name: $0.key, value: $0.value) }
35 | return components
36 | }
37 | }
38 |
39 | extension URLRequest {
40 |
41 | static func unsplash(url: URL) -> URLRequest {
42 | var request = URLRequest(url: url)
43 | request.setValue("v1", forHTTPHeaderField: "Accept-Version")
44 | fatalError("create your own unsplash api key -> https://unsplash.com/documentation#creating-a-developer-account")
45 | request.setValue("[ add your unsplash api key here ]", forHTTPHeaderField: "Authorization")
46 | return request
47 | }
48 |
49 | static func searchPhotos(query: String, perPage: Int = 20) -> URLRequest {
50 | let url = URLComponents.unsplash(path: "/search/photos", queryItems: ["query": query, "per_page": "\(perPage)"]).url!
51 | return .unsplash(url: url)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/CombineUIKit/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/CombineUIKit/Resources/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 | UISceneConfigurations
28 |
29 | UIWindowSceneSessionRoleApplication
30 |
31 |
32 | UISceneConfigurationName
33 | Default Configuration
34 | UISceneDelegateClassName
35 | $(PRODUCT_MODULE_NAME).SceneDelegate
36 | UISceneStoryboardFile
37 | Search
38 |
39 |
40 |
41 |
42 | UIApplicationSupportsIndirectInputEvents
43 |
44 | UILaunchStoryboardName
45 | LaunchScreen
46 | UIMainStoryboardFile
47 | Search
48 | UIRequiredDeviceCapabilities
49 |
50 | armv7
51 |
52 | UISupportedInterfaceOrientations
53 |
54 | UIInterfaceOrientationPortrait
55 | UIInterfaceOrientationLandscapeLeft
56 | UIInterfaceOrientationLandscapeRight
57 |
58 | UISupportedInterfaceOrientations~ipad
59 |
60 | UIInterfaceOrientationPortrait
61 | UIInterfaceOrientationPortraitUpsideDown
62 | UIInterfaceOrientationLandscapeLeft
63 | UIInterfaceOrientationLandscapeRight
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Combine + UIKit + MVVM
2 |
3 | An example of how to bind views / view controllers and view models using Combine and UIKit in a fully reactive way. (If you wish to run this demo, you will need to create a developer account at [Unsplash][1] and add your client ID in Requests.swift).
4 |
5 | Checkout the full post on [tapdev][2]
6 |
7 | [1]: https://unsplash.com/documentation#creating-a-developer-account
8 | [2]: https://tapdev.co/2021/03/31/a-better-way-to-structure-combine-with-uikit-in-mvvm/
9 |
10 | 
11 |
12 | The view model has a single bind function, where all of the inputs are transformed into outputs.
13 |
14 | The inputs are user events, which in the case of this demo is a single publisher which feeds the view model with the stream of text coming from the search bar. The outputs are then generated by the view model, and made available for subscription in the view using the ```@Published``` property wrapper. Note that there are *no subscriptions being collected in the view model*. This is made possible using ```assign(to:)``` operator which republishes values to the underlying publishers of ```@Published``` properties. The outputs in this case are an array of photo model objects (the result of the API search) and a boolean value to indicate if search is taking place.
15 |
16 | ```swift
17 | final class SearchViewModel {
18 |
19 | @Published var photos: [Photo] = []
20 | @Published var searching: Bool = false
21 |
22 | func bind(searchQuery: AnyPublisher) {
23 |
24 | let search = searchQuery
25 | .debounce(for: .seconds(0.5), scheduler: DispatchQueue.global())
26 | .map { URLRequest.searchPhotos(query: $0) }
27 | .share()
28 |
29 | let photos = search
30 | .map { API.publisher(for: $0) }
31 | .switchToLatest()
32 | .decode(type: SearchPhotos.self, decoder: API.jsonDecoder)
33 | .replaceError(with: .emptyResults)
34 | .share()
35 |
36 | photos
37 | .map(\.results)
38 | .receive(on: DispatchQueue.main)
39 | .assign(to: &$photos)
40 |
41 | search
42 | .map { _ in true }
43 | .merge(with: photos
44 | .map { _ in false }
45 | .replaceError(with: false)
46 | .receive(on: DispatchQueue.main)
47 | .assign(to: &$searching)
48 | }
49 | }
50 | ```
51 |
52 | The view controller then subscribes to the outputs of the view model.
53 |
54 | ```swift
55 | final class SearchViewController: UIViewController {
56 |
57 | @IBOutlet weak var activityView: UIActivityIndicatorView!
58 | @IBOutlet weak var searchBar: UISearchBar!
59 | @IBOutlet weak var collectionView: UICollectionView!
60 |
61 | private let viewModel = SearchViewModel()
62 | private var subscriptions = Set()
63 |
64 | override func viewDidLoad() {
65 | super.viewDidLoad()
66 |
67 | viewModel.bind(searchQuery: searchBar.textDidChangePublisher)
68 |
69 | viewModel.$photos
70 | .bind(subscriber: collectionView.itemsSubscriber(cellIdentifier: "Cell", cellType: PhotoCell.self, cellConfig: { cell, _, photo in
71 | cell.bind(photo)
72 | }))
73 | .store(in: &subscriptions)
74 |
75 | viewModel.$searching
76 | .sink { [weak activityView] searching in
77 | searching ? activityView?.startAnimating() : activityView?.stopAnimating()
78 | }
79 | .store(in: &subscriptions)
80 |
81 | searchBar.searchButtonClickedPublisher
82 | .sink { [weak searchBar] in
83 | searchBar?.resignFirstResponder()
84 | }
85 | .store(in: &subscriptions)
86 | }
87 | }
88 | ```
89 |
--------------------------------------------------------------------------------
/CombineUIKit/Scenes/Search/CustomGridLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomGridLayout.swift
3 | // RxRover
4 | //
5 | // Created by Greg Price on 01/03/2021.
6 | //
7 |
8 | import UIKit
9 |
10 | // Ref: "For a Complex Grid, Define Cell Sizes Explicitly"
11 | // https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/layouts/customizing_collection_view_layouts
12 |
13 | enum CustomGridSegmentStyle {
14 | case oneThirdTwoThirds
15 | case twoThirdsOneThird
16 | case fullWidth
17 | }
18 |
19 | class CustomGridLayout: UICollectionViewLayout {
20 |
21 | private var contentBounds = CGRect.zero
22 | private var cachedAttributes = [UICollectionViewLayoutAttributes]()
23 |
24 | var headerHeight: CGFloat = 0
25 | var headerPadding: CGFloat = 10
26 | var segmentPadding: CGFloat = 10
27 | var segmentHeight: CGFloat = 240
28 |
29 | override func prepare() {
30 | super.prepare()
31 | guard let collectionView = collectionView else { return }
32 | collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: segmentPadding + 94, right: 0)
33 | resetCacheInfo(collectionView: collectionView)
34 | calculateGeometry(collectionView: collectionView)
35 | }
36 |
37 | private func resetCacheInfo(collectionView: UICollectionView) {
38 | cachedAttributes.removeAll()
39 | contentBounds = CGRect(origin: .zero, size: collectionView.bounds.size)
40 | }
41 |
42 | private func calculateGeometry(collectionView: UICollectionView) {
43 | let count = collectionView.numberOfItems(inSection: 0)
44 | var currentIndex = 0
45 | var segmentStyle: CustomGridSegmentStyle = count == 1 ? .fullWidth : .twoThirdsOneThird
46 | var lastFrame: CGRect = .zero
47 | let cvWidth = collectionView.bounds.size.width
48 |
49 | while currentIndex < count {
50 | var segmentFrame: CGRect = .zero
51 | if currentIndex == 0 {
52 | segmentFrame = CGRect(x: segmentPadding, y: lastFrame.maxY + headerHeight + headerPadding, width: cvWidth - (segmentPadding * 2), height: segmentHeight)
53 | } else {
54 | segmentFrame = CGRect(x: segmentPadding, y: lastFrame.maxY + segmentPadding, width: cvWidth - (segmentPadding * 2), height: segmentHeight)
55 | }
56 |
57 | var segmentRects = [CGRect]()
58 | switch segmentStyle {
59 |
60 | case .oneThirdTwoThirds:
61 | let horizontalSlices = segmentFrame.dividedIntegral(fraction: (1.0 / 3.0), from: .minXEdge, padding: segmentPadding)
62 | let verticalSlices = horizontalSlices.first.dividedIntegral(fraction: 0.5, from: .minYEdge, padding: segmentPadding)
63 | segmentRects = [verticalSlices.first, horizontalSlices.second, verticalSlices.second]
64 |
65 | case .twoThirdsOneThird:
66 | let horizontalSlices = segmentFrame.dividedIntegral(fraction: (2.0 / 3.0), from: .minXEdge, padding: segmentPadding)
67 | let verticalSlices = horizontalSlices.second.dividedIntegral(fraction: 0.5, from: .minYEdge, padding: segmentPadding)
68 | segmentRects = [horizontalSlices.first, verticalSlices.first, verticalSlices.second]
69 |
70 | case .fullWidth:
71 | segmentRects = [segmentFrame]
72 | }
73 |
74 | for rect in segmentRects {
75 | if currentIndex < count {
76 | let attributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: currentIndex, section: 0))
77 | attributes.frame = rect
78 | lastFrame = attributes.frame
79 | cachedAttributes.append(attributes)
80 | contentBounds = contentBounds.union(lastFrame)
81 | currentIndex += 1
82 | }
83 | }
84 |
85 | let countModulo = count % 3
86 | let remaining = count - currentIndex
87 | if countModulo == 0 || (countModulo != 0 && remaining != 1) {
88 | if segmentStyle == .oneThirdTwoThirds {
89 | segmentStyle = .twoThirdsOneThird
90 | } else if segmentStyle == .twoThirdsOneThird {
91 | segmentStyle = .oneThirdTwoThirds
92 | }
93 | } else {
94 | segmentStyle = .fullWidth
95 | }
96 | }
97 | }
98 |
99 | override var collectionViewContentSize: CGSize {
100 | return contentBounds.size
101 | }
102 |
103 | override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
104 | guard let collectionView = collectionView else { return false }
105 | return !newBounds.size.equalTo(collectionView.bounds.size)
106 | }
107 |
108 | override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
109 | return cachedAttributes[indexPath.item]
110 | }
111 |
112 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
113 | var attributesArray = [UICollectionViewLayoutAttributes]()
114 | guard let lastIndex = cachedAttributes.indices.last,
115 | let firstMatchIndex = binSearch(rect, start: 0, end: lastIndex) else { return attributesArray }
116 |
117 | for attributes in cachedAttributes[..= rect.minY else { break }
119 | attributesArray.append(attributes)
120 | }
121 |
122 | for attributes in cachedAttributes[firstMatchIndex...] {
123 | guard attributes.frame.minY <= rect.maxY else { break }
124 | attributesArray.append(attributes)
125 | }
126 |
127 | return attributesArray
128 | }
129 |
130 | private func binSearch(_ rect: CGRect, start: Int, end: Int) -> Int? {
131 | if end < start { return nil }
132 | let mid = (start + end) / 2
133 | let attr = cachedAttributes[mid]
134 | if attr.frame.intersects(rect) {
135 | return mid
136 | } else {
137 | if attr.frame.maxY < rect.minY {
138 | return binSearch(rect, start: (mid + 1), end: end)
139 | } else {
140 | return binSearch(rect, start: start, end: (mid - 1))
141 | }
142 | }
143 | }
144 | }
145 |
146 | // https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/layouts/customizing_collection_view_layouts
147 |
148 | extension CGRect {
149 |
150 | func dividedIntegral(fraction: CGFloat, from fromEdge: CGRectEdge, padding: CGFloat = 10) -> (first: CGRect, second: CGRect) {
151 | let dimension: CGFloat
152 | switch fromEdge {
153 |
154 | case .minXEdge, .maxXEdge:
155 | dimension = self.size.width
156 |
157 | case .minYEdge, .maxYEdge:
158 | dimension = self.size.height
159 | }
160 |
161 | let distance = (dimension * fraction).rounded(.up)
162 | var slices = self.divided(atDistance: distance, from: fromEdge)
163 | switch fromEdge {
164 |
165 | case .minXEdge, .maxXEdge:
166 | slices.remainder.origin.x += padding
167 | slices.remainder.size.width -= padding
168 |
169 | case .minYEdge, .maxYEdge:
170 | slices.remainder.origin.y += padding
171 | slices.remainder.size.height -= padding
172 | }
173 |
174 | return (first: slices.slice, second: slices.remainder)
175 | }
176 | }
177 |
178 |
--------------------------------------------------------------------------------
/CombineUIKit/Scenes/Search/Base.lproj/Search.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/CombineUIKit.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | F395A95F2613654100431A56 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F395A95E2613654100431A56 /* AppDelegate.swift */; };
11 | F395A9612613654100431A56 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F395A9602613654100431A56 /* SceneDelegate.swift */; };
12 | F395A9662613654100431A56 /* Search.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F395A9642613654100431A56 /* Search.storyboard */; };
13 | F395A9682613654400431A56 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F395A9672613654400431A56 /* Assets.xcassets */; };
14 | F395A96B2613654400431A56 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F395A9692613654400431A56 /* LaunchScreen.storyboard */; };
15 | F395A9762613654400431A56 /* CombineUIKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F395A9752613654400431A56 /* CombineUIKitTests.swift */; };
16 | F395A99B2613662400431A56 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F395A99A2613662400431A56 /* SearchViewController.swift */; };
17 | F395A99F2613664700431A56 /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F395A99E2613664700431A56 /* SearchViewModel.swift */; };
18 | F395A9AD261368C900431A56 /* Photo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F395A9AC261368C900431A56 /* Photo.swift */; };
19 | F395A9B1261369B000431A56 /* Requests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F395A9B0261369B000431A56 /* Requests.swift */; };
20 | F3E481F52613A8BA00A2E419 /* CombineCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = F3E481F42613A8BA00A2E419 /* CombineCocoa */; };
21 | F3E481F92614824D00A2E419 /* CustomGridLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E481F82614824D00A2E419 /* CustomGridLayout.swift */; };
22 | F3E481FE261482FD00A2E419 /* CombineDataSources in Frameworks */ = {isa = PBXBuildFile; productRef = F3E481FD261482FD00A2E419 /* CombineDataSources */; };
23 | F3E4820326148CB200A2E419 /* PhotoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E4820226148CB200A2E419 /* PhotoCell.swift */; };
24 | /* End PBXBuildFile section */
25 |
26 | /* Begin PBXContainerItemProxy section */
27 | F395A9722613654400431A56 /* PBXContainerItemProxy */ = {
28 | isa = PBXContainerItemProxy;
29 | containerPortal = F395A9532613654100431A56 /* Project object */;
30 | proxyType = 1;
31 | remoteGlobalIDString = F395A95A2613654100431A56;
32 | remoteInfo = CombineUIKit;
33 | };
34 | /* End PBXContainerItemProxy section */
35 |
36 | /* Begin PBXFileReference section */
37 | F395A95B2613654100431A56 /* CombineUIKit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CombineUIKit.app; sourceTree = BUILT_PRODUCTS_DIR; };
38 | F395A95E2613654100431A56 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
39 | F395A9602613654100431A56 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
40 | F395A9652613654100431A56 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Search.storyboard; sourceTree = ""; };
41 | F395A9672613654400431A56 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
42 | F395A96A2613654400431A56 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
43 | F395A96C2613654400431A56 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
44 | F395A9712613654400431A56 /* CombineUIKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CombineUIKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
45 | F395A9752613654400431A56 /* CombineUIKitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineUIKitTests.swift; sourceTree = ""; };
46 | F395A9772613654400431A56 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
47 | F395A99A2613662400431A56 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; };
48 | F395A99E2613664700431A56 /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; };
49 | F395A9AC261368C900431A56 /* Photo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Photo.swift; sourceTree = ""; };
50 | F395A9B0261369B000431A56 /* Requests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Requests.swift; sourceTree = ""; };
51 | F3E481F82614824D00A2E419 /* CustomGridLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomGridLayout.swift; sourceTree = ""; };
52 | F3E4820226148CB200A2E419 /* PhotoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoCell.swift; sourceTree = ""; };
53 | /* End PBXFileReference section */
54 |
55 | /* Begin PBXFrameworksBuildPhase section */
56 | F395A9582613654100431A56 /* Frameworks */ = {
57 | isa = PBXFrameworksBuildPhase;
58 | buildActionMask = 2147483647;
59 | files = (
60 | F3E481F52613A8BA00A2E419 /* CombineCocoa in Frameworks */,
61 | F3E481FE261482FD00A2E419 /* CombineDataSources in Frameworks */,
62 | );
63 | runOnlyForDeploymentPostprocessing = 0;
64 | };
65 | F395A96E2613654400431A56 /* Frameworks */ = {
66 | isa = PBXFrameworksBuildPhase;
67 | buildActionMask = 2147483647;
68 | files = (
69 | );
70 | runOnlyForDeploymentPostprocessing = 0;
71 | };
72 | /* End PBXFrameworksBuildPhase section */
73 |
74 | /* Begin PBXGroup section */
75 | F395A9522613654100431A56 = {
76 | isa = PBXGroup;
77 | children = (
78 | F395A95D2613654100431A56 /* CombineUIKit */,
79 | F395A9742613654400431A56 /* CombineUIKitTests */,
80 | F395A95C2613654100431A56 /* Products */,
81 | );
82 | sourceTree = "";
83 | };
84 | F395A95C2613654100431A56 /* Products */ = {
85 | isa = PBXGroup;
86 | children = (
87 | F395A95B2613654100431A56 /* CombineUIKit.app */,
88 | F395A9712613654400431A56 /* CombineUIKitTests.xctest */,
89 | );
90 | name = Products;
91 | sourceTree = "";
92 | };
93 | F395A95D2613654100431A56 /* CombineUIKit */ = {
94 | isa = PBXGroup;
95 | children = (
96 | F3E48208261491D900A2E419 /* App */,
97 | F395A9962613659500431A56 /* Scenes */,
98 | F395A999261365E300431A56 /* Shared */,
99 | F395A998261365A900431A56 /* Resources */,
100 | );
101 | path = CombineUIKit;
102 | sourceTree = "";
103 | };
104 | F395A9742613654400431A56 /* CombineUIKitTests */ = {
105 | isa = PBXGroup;
106 | children = (
107 | F395A9752613654400431A56 /* CombineUIKitTests.swift */,
108 | F395A9772613654400431A56 /* Info.plist */,
109 | );
110 | path = CombineUIKitTests;
111 | sourceTree = "";
112 | };
113 | F395A9962613659500431A56 /* Scenes */ = {
114 | isa = PBXGroup;
115 | children = (
116 | F395A9972613659D00431A56 /* Search */,
117 | );
118 | path = Scenes;
119 | sourceTree = "";
120 | };
121 | F395A9972613659D00431A56 /* Search */ = {
122 | isa = PBXGroup;
123 | children = (
124 | F395A9642613654100431A56 /* Search.storyboard */,
125 | F395A99A2613662400431A56 /* SearchViewController.swift */,
126 | F395A99E2613664700431A56 /* SearchViewModel.swift */,
127 | F3E481F82614824D00A2E419 /* CustomGridLayout.swift */,
128 | F3E4820226148CB200A2E419 /* PhotoCell.swift */,
129 | );
130 | path = Search;
131 | sourceTree = "";
132 | };
133 | F395A998261365A900431A56 /* Resources */ = {
134 | isa = PBXGroup;
135 | children = (
136 | F395A9672613654400431A56 /* Assets.xcassets */,
137 | F395A9692613654400431A56 /* LaunchScreen.storyboard */,
138 | F395A96C2613654400431A56 /* Info.plist */,
139 | );
140 | path = Resources;
141 | sourceTree = "";
142 | };
143 | F395A999261365E300431A56 /* Shared */ = {
144 | isa = PBXGroup;
145 | children = (
146 | F395A9AC261368C900431A56 /* Photo.swift */,
147 | F395A9B0261369B000431A56 /* Requests.swift */,
148 | );
149 | path = Shared;
150 | sourceTree = "";
151 | };
152 | F3E48208261491D900A2E419 /* App */ = {
153 | isa = PBXGroup;
154 | children = (
155 | F395A95E2613654100431A56 /* AppDelegate.swift */,
156 | F395A9602613654100431A56 /* SceneDelegate.swift */,
157 | );
158 | path = App;
159 | sourceTree = "";
160 | };
161 | /* End PBXGroup section */
162 |
163 | /* Begin PBXNativeTarget section */
164 | F395A95A2613654100431A56 /* CombineUIKit */ = {
165 | isa = PBXNativeTarget;
166 | buildConfigurationList = F395A9852613654500431A56 /* Build configuration list for PBXNativeTarget "CombineUIKit" */;
167 | buildPhases = (
168 | F395A9572613654100431A56 /* Sources */,
169 | F395A9582613654100431A56 /* Frameworks */,
170 | F395A9592613654100431A56 /* Resources */,
171 | );
172 | buildRules = (
173 | );
174 | dependencies = (
175 | );
176 | name = CombineUIKit;
177 | packageProductDependencies = (
178 | F3E481F42613A8BA00A2E419 /* CombineCocoa */,
179 | F3E481FD261482FD00A2E419 /* CombineDataSources */,
180 | );
181 | productName = CombineUIKit;
182 | productReference = F395A95B2613654100431A56 /* CombineUIKit.app */;
183 | productType = "com.apple.product-type.application";
184 | };
185 | F395A9702613654400431A56 /* CombineUIKitTests */ = {
186 | isa = PBXNativeTarget;
187 | buildConfigurationList = F395A9882613654500431A56 /* Build configuration list for PBXNativeTarget "CombineUIKitTests" */;
188 | buildPhases = (
189 | F395A96D2613654400431A56 /* Sources */,
190 | F395A96E2613654400431A56 /* Frameworks */,
191 | F395A96F2613654400431A56 /* Resources */,
192 | );
193 | buildRules = (
194 | );
195 | dependencies = (
196 | F395A9732613654400431A56 /* PBXTargetDependency */,
197 | );
198 | name = CombineUIKitTests;
199 | productName = CombineUIKitTests;
200 | productReference = F395A9712613654400431A56 /* CombineUIKitTests.xctest */;
201 | productType = "com.apple.product-type.bundle.unit-test";
202 | };
203 | /* End PBXNativeTarget section */
204 |
205 | /* Begin PBXProject section */
206 | F395A9532613654100431A56 /* Project object */ = {
207 | isa = PBXProject;
208 | attributes = {
209 | LastSwiftUpdateCheck = 1240;
210 | LastUpgradeCheck = 1240;
211 | TargetAttributes = {
212 | F395A95A2613654100431A56 = {
213 | CreatedOnToolsVersion = 12.4;
214 | };
215 | F395A9702613654400431A56 = {
216 | CreatedOnToolsVersion = 12.4;
217 | TestTargetID = F395A95A2613654100431A56;
218 | };
219 | };
220 | };
221 | buildConfigurationList = F395A9562613654100431A56 /* Build configuration list for PBXProject "CombineUIKit" */;
222 | compatibilityVersion = "Xcode 9.3";
223 | developmentRegion = en;
224 | hasScannedForEncodings = 0;
225 | knownRegions = (
226 | en,
227 | Base,
228 | );
229 | mainGroup = F395A9522613654100431A56;
230 | packageReferences = (
231 | F3E481F32613A8BA00A2E419 /* XCRemoteSwiftPackageReference "CombineCocoa" */,
232 | F3E481FC261482FD00A2E419 /* XCRemoteSwiftPackageReference "CombineDataSources" */,
233 | );
234 | productRefGroup = F395A95C2613654100431A56 /* Products */;
235 | projectDirPath = "";
236 | projectRoot = "";
237 | targets = (
238 | F395A95A2613654100431A56 /* CombineUIKit */,
239 | F395A9702613654400431A56 /* CombineUIKitTests */,
240 | );
241 | };
242 | /* End PBXProject section */
243 |
244 | /* Begin PBXResourcesBuildPhase section */
245 | F395A9592613654100431A56 /* Resources */ = {
246 | isa = PBXResourcesBuildPhase;
247 | buildActionMask = 2147483647;
248 | files = (
249 | F395A96B2613654400431A56 /* LaunchScreen.storyboard in Resources */,
250 | F395A9682613654400431A56 /* Assets.xcassets in Resources */,
251 | F395A9662613654100431A56 /* Search.storyboard in Resources */,
252 | );
253 | runOnlyForDeploymentPostprocessing = 0;
254 | };
255 | F395A96F2613654400431A56 /* Resources */ = {
256 | isa = PBXResourcesBuildPhase;
257 | buildActionMask = 2147483647;
258 | files = (
259 | );
260 | runOnlyForDeploymentPostprocessing = 0;
261 | };
262 | /* End PBXResourcesBuildPhase section */
263 |
264 | /* Begin PBXSourcesBuildPhase section */
265 | F395A9572613654100431A56 /* Sources */ = {
266 | isa = PBXSourcesBuildPhase;
267 | buildActionMask = 2147483647;
268 | files = (
269 | F395A99F2613664700431A56 /* SearchViewModel.swift in Sources */,
270 | F395A95F2613654100431A56 /* AppDelegate.swift in Sources */,
271 | F395A99B2613662400431A56 /* SearchViewController.swift in Sources */,
272 | F395A9B1261369B000431A56 /* Requests.swift in Sources */,
273 | F3E4820326148CB200A2E419 /* PhotoCell.swift in Sources */,
274 | F395A9612613654100431A56 /* SceneDelegate.swift in Sources */,
275 | F3E481F92614824D00A2E419 /* CustomGridLayout.swift in Sources */,
276 | F395A9AD261368C900431A56 /* Photo.swift in Sources */,
277 | );
278 | runOnlyForDeploymentPostprocessing = 0;
279 | };
280 | F395A96D2613654400431A56 /* Sources */ = {
281 | isa = PBXSourcesBuildPhase;
282 | buildActionMask = 2147483647;
283 | files = (
284 | F395A9762613654400431A56 /* CombineUIKitTests.swift in Sources */,
285 | );
286 | runOnlyForDeploymentPostprocessing = 0;
287 | };
288 | /* End PBXSourcesBuildPhase section */
289 |
290 | /* Begin PBXTargetDependency section */
291 | F395A9732613654400431A56 /* PBXTargetDependency */ = {
292 | isa = PBXTargetDependency;
293 | target = F395A95A2613654100431A56 /* CombineUIKit */;
294 | targetProxy = F395A9722613654400431A56 /* PBXContainerItemProxy */;
295 | };
296 | /* End PBXTargetDependency section */
297 |
298 | /* Begin PBXVariantGroup section */
299 | F395A9642613654100431A56 /* Search.storyboard */ = {
300 | isa = PBXVariantGroup;
301 | children = (
302 | F395A9652613654100431A56 /* Base */,
303 | );
304 | name = Search.storyboard;
305 | sourceTree = "";
306 | };
307 | F395A9692613654400431A56 /* LaunchScreen.storyboard */ = {
308 | isa = PBXVariantGroup;
309 | children = (
310 | F395A96A2613654400431A56 /* Base */,
311 | );
312 | name = LaunchScreen.storyboard;
313 | sourceTree = "";
314 | };
315 | /* End PBXVariantGroup section */
316 |
317 | /* Begin XCBuildConfiguration section */
318 | F395A9832613654500431A56 /* Debug */ = {
319 | isa = XCBuildConfiguration;
320 | buildSettings = {
321 | ALWAYS_SEARCH_USER_PATHS = NO;
322 | CLANG_ANALYZER_NONNULL = YES;
323 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
324 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
325 | CLANG_CXX_LIBRARY = "libc++";
326 | CLANG_ENABLE_MODULES = YES;
327 | CLANG_ENABLE_OBJC_ARC = YES;
328 | CLANG_ENABLE_OBJC_WEAK = YES;
329 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
330 | CLANG_WARN_BOOL_CONVERSION = YES;
331 | CLANG_WARN_COMMA = YES;
332 | CLANG_WARN_CONSTANT_CONVERSION = YES;
333 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
334 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
335 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
336 | CLANG_WARN_EMPTY_BODY = YES;
337 | CLANG_WARN_ENUM_CONVERSION = YES;
338 | CLANG_WARN_INFINITE_RECURSION = YES;
339 | CLANG_WARN_INT_CONVERSION = YES;
340 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
341 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
342 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
343 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
344 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
345 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
346 | CLANG_WARN_STRICT_PROTOTYPES = YES;
347 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
348 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
349 | CLANG_WARN_UNREACHABLE_CODE = YES;
350 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
351 | COPY_PHASE_STRIP = NO;
352 | DEBUG_INFORMATION_FORMAT = dwarf;
353 | ENABLE_STRICT_OBJC_MSGSEND = YES;
354 | ENABLE_TESTABILITY = YES;
355 | GCC_C_LANGUAGE_STANDARD = gnu11;
356 | GCC_DYNAMIC_NO_PIC = NO;
357 | GCC_NO_COMMON_BLOCKS = YES;
358 | GCC_OPTIMIZATION_LEVEL = 0;
359 | GCC_PREPROCESSOR_DEFINITIONS = (
360 | "DEBUG=1",
361 | "$(inherited)",
362 | );
363 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
364 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
365 | GCC_WARN_UNDECLARED_SELECTOR = YES;
366 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
367 | GCC_WARN_UNUSED_FUNCTION = YES;
368 | GCC_WARN_UNUSED_VARIABLE = YES;
369 | IPHONEOS_DEPLOYMENT_TARGET = 14.4;
370 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
371 | MTL_FAST_MATH = YES;
372 | ONLY_ACTIVE_ARCH = YES;
373 | SDKROOT = iphoneos;
374 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
375 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
376 | };
377 | name = Debug;
378 | };
379 | F395A9842613654500431A56 /* Release */ = {
380 | isa = XCBuildConfiguration;
381 | buildSettings = {
382 | ALWAYS_SEARCH_USER_PATHS = NO;
383 | CLANG_ANALYZER_NONNULL = YES;
384 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
385 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
386 | CLANG_CXX_LIBRARY = "libc++";
387 | CLANG_ENABLE_MODULES = YES;
388 | CLANG_ENABLE_OBJC_ARC = YES;
389 | CLANG_ENABLE_OBJC_WEAK = YES;
390 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
391 | CLANG_WARN_BOOL_CONVERSION = YES;
392 | CLANG_WARN_COMMA = YES;
393 | CLANG_WARN_CONSTANT_CONVERSION = YES;
394 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
395 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
396 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
397 | CLANG_WARN_EMPTY_BODY = YES;
398 | CLANG_WARN_ENUM_CONVERSION = YES;
399 | CLANG_WARN_INFINITE_RECURSION = YES;
400 | CLANG_WARN_INT_CONVERSION = YES;
401 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
402 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
403 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
404 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
405 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
406 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
407 | CLANG_WARN_STRICT_PROTOTYPES = YES;
408 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
409 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
410 | CLANG_WARN_UNREACHABLE_CODE = YES;
411 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
412 | COPY_PHASE_STRIP = NO;
413 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
414 | ENABLE_NS_ASSERTIONS = NO;
415 | ENABLE_STRICT_OBJC_MSGSEND = YES;
416 | GCC_C_LANGUAGE_STANDARD = gnu11;
417 | GCC_NO_COMMON_BLOCKS = YES;
418 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
419 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
420 | GCC_WARN_UNDECLARED_SELECTOR = YES;
421 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
422 | GCC_WARN_UNUSED_FUNCTION = YES;
423 | GCC_WARN_UNUSED_VARIABLE = YES;
424 | IPHONEOS_DEPLOYMENT_TARGET = 14.4;
425 | MTL_ENABLE_DEBUG_INFO = NO;
426 | MTL_FAST_MATH = YES;
427 | SDKROOT = iphoneos;
428 | SWIFT_COMPILATION_MODE = wholemodule;
429 | SWIFT_OPTIMIZATION_LEVEL = "-O";
430 | VALIDATE_PRODUCT = YES;
431 | };
432 | name = Release;
433 | };
434 | F395A9862613654500431A56 /* Debug */ = {
435 | isa = XCBuildConfiguration;
436 | buildSettings = {
437 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
438 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
439 | CODE_SIGN_STYLE = Automatic;
440 | INFOPLIST_FILE = CombineUIKit/Resources/Info.plist;
441 | LD_RUNPATH_SEARCH_PATHS = (
442 | "$(inherited)",
443 | "@executable_path/Frameworks",
444 | );
445 | PRODUCT_BUNDLE_IDENTIFIER = co.tapdev.CombineUIKit;
446 | PRODUCT_NAME = "$(TARGET_NAME)";
447 | SWIFT_VERSION = 5.0;
448 | TARGETED_DEVICE_FAMILY = "1,2";
449 | };
450 | name = Debug;
451 | };
452 | F395A9872613654500431A56 /* Release */ = {
453 | isa = XCBuildConfiguration;
454 | buildSettings = {
455 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
456 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
457 | CODE_SIGN_STYLE = Automatic;
458 | INFOPLIST_FILE = CombineUIKit/Resources/Info.plist;
459 | LD_RUNPATH_SEARCH_PATHS = (
460 | "$(inherited)",
461 | "@executable_path/Frameworks",
462 | );
463 | PRODUCT_BUNDLE_IDENTIFIER = co.tapdev.CombineUIKit;
464 | PRODUCT_NAME = "$(TARGET_NAME)";
465 | SWIFT_VERSION = 5.0;
466 | TARGETED_DEVICE_FAMILY = "1,2";
467 | };
468 | name = Release;
469 | };
470 | F395A9892613654500431A56 /* Debug */ = {
471 | isa = XCBuildConfiguration;
472 | buildSettings = {
473 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
474 | BUNDLE_LOADER = "$(TEST_HOST)";
475 | CODE_SIGN_STYLE = Automatic;
476 | INFOPLIST_FILE = CombineUIKitTests/Info.plist;
477 | IPHONEOS_DEPLOYMENT_TARGET = 14.4;
478 | LD_RUNPATH_SEARCH_PATHS = (
479 | "$(inherited)",
480 | "@executable_path/Frameworks",
481 | "@loader_path/Frameworks",
482 | );
483 | PRODUCT_BUNDLE_IDENTIFIER = co.tapdev.CombineUIKitTests;
484 | PRODUCT_NAME = "$(TARGET_NAME)";
485 | SWIFT_VERSION = 5.0;
486 | TARGETED_DEVICE_FAMILY = "1,2";
487 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CombineUIKit.app/CombineUIKit";
488 | };
489 | name = Debug;
490 | };
491 | F395A98A2613654500431A56 /* Release */ = {
492 | isa = XCBuildConfiguration;
493 | buildSettings = {
494 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
495 | BUNDLE_LOADER = "$(TEST_HOST)";
496 | CODE_SIGN_STYLE = Automatic;
497 | INFOPLIST_FILE = CombineUIKitTests/Info.plist;
498 | IPHONEOS_DEPLOYMENT_TARGET = 14.4;
499 | LD_RUNPATH_SEARCH_PATHS = (
500 | "$(inherited)",
501 | "@executable_path/Frameworks",
502 | "@loader_path/Frameworks",
503 | );
504 | PRODUCT_BUNDLE_IDENTIFIER = co.tapdev.CombineUIKitTests;
505 | PRODUCT_NAME = "$(TARGET_NAME)";
506 | SWIFT_VERSION = 5.0;
507 | TARGETED_DEVICE_FAMILY = "1,2";
508 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CombineUIKit.app/CombineUIKit";
509 | };
510 | name = Release;
511 | };
512 | /* End XCBuildConfiguration section */
513 |
514 | /* Begin XCConfigurationList section */
515 | F395A9562613654100431A56 /* Build configuration list for PBXProject "CombineUIKit" */ = {
516 | isa = XCConfigurationList;
517 | buildConfigurations = (
518 | F395A9832613654500431A56 /* Debug */,
519 | F395A9842613654500431A56 /* Release */,
520 | );
521 | defaultConfigurationIsVisible = 0;
522 | defaultConfigurationName = Release;
523 | };
524 | F395A9852613654500431A56 /* Build configuration list for PBXNativeTarget "CombineUIKit" */ = {
525 | isa = XCConfigurationList;
526 | buildConfigurations = (
527 | F395A9862613654500431A56 /* Debug */,
528 | F395A9872613654500431A56 /* Release */,
529 | );
530 | defaultConfigurationIsVisible = 0;
531 | defaultConfigurationName = Release;
532 | };
533 | F395A9882613654500431A56 /* Build configuration list for PBXNativeTarget "CombineUIKitTests" */ = {
534 | isa = XCConfigurationList;
535 | buildConfigurations = (
536 | F395A9892613654500431A56 /* Debug */,
537 | F395A98A2613654500431A56 /* Release */,
538 | );
539 | defaultConfigurationIsVisible = 0;
540 | defaultConfigurationName = Release;
541 | };
542 | /* End XCConfigurationList section */
543 |
544 | /* Begin XCRemoteSwiftPackageReference section */
545 | F3E481F32613A8BA00A2E419 /* XCRemoteSwiftPackageReference "CombineCocoa" */ = {
546 | isa = XCRemoteSwiftPackageReference;
547 | repositoryURL = "https://github.com/CombineCommunity/CombineCocoa";
548 | requirement = {
549 | kind = upToNextMajorVersion;
550 | minimumVersion = 0.2.2;
551 | };
552 | };
553 | F3E481FC261482FD00A2E419 /* XCRemoteSwiftPackageReference "CombineDataSources" */ = {
554 | isa = XCRemoteSwiftPackageReference;
555 | repositoryURL = "https://github.com/CombineCommunity/CombineDataSources";
556 | requirement = {
557 | kind = upToNextMajorVersion;
558 | minimumVersion = 0.2.5;
559 | };
560 | };
561 | /* End XCRemoteSwiftPackageReference section */
562 |
563 | /* Begin XCSwiftPackageProductDependency section */
564 | F3E481F42613A8BA00A2E419 /* CombineCocoa */ = {
565 | isa = XCSwiftPackageProductDependency;
566 | package = F3E481F32613A8BA00A2E419 /* XCRemoteSwiftPackageReference "CombineCocoa" */;
567 | productName = CombineCocoa;
568 | };
569 | F3E481FD261482FD00A2E419 /* CombineDataSources */ = {
570 | isa = XCSwiftPackageProductDependency;
571 | package = F3E481FC261482FD00A2E419 /* XCRemoteSwiftPackageReference "CombineDataSources" */;
572 | productName = CombineDataSources;
573 | };
574 | /* End XCSwiftPackageProductDependency section */
575 | };
576 | rootObject = F395A9532613654100431A56 /* Project object */;
577 | }
578 |
--------------------------------------------------------------------------------