├── SwiftUI-SDUI ├── Assets.xcassets │ ├── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── repository │ ├── api_models │ │ ├── GenreResult.swift │ │ ├── TvShowsResult.swift │ │ ├── MoviesResult.swift │ │ └── LanguageResult.swift │ ├── TmDbRepository.swift │ └── BaseRepository.swift ├── UIComponents │ ├── UIComponent.swift │ ├── GenreListComponent.swift │ ├── LanguageListComponent.swift │ ├── MovieListComponent.swift │ ├── TvShowsListComponent.swift │ └── SubscriptionComponent.swift ├── HomePageView.swift ├── AppDelegate.swift ├── Base.lproj │ └── LaunchScreen.storyboard ├── Info.plist ├── HomePage │ └── HomePageController.swift └── SceneDelegate.swift ├── SwiftUI-SDUI.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcuserdata │ └── porter.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── project.pbxproj └── README.md /SwiftUI-SDUI/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /SwiftUI-SDUI/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /SwiftUI-SDUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftUI-SDUI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftUI-SDUI/repository/api_models/GenreResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenreResult.swift 3 | // SwiftUI-SDUI 4 | // 5 | // Created by porter on 07/05/20. 6 | // Copyright © 2020 kinley. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct GenreResult: Codable { 12 | let genres: [Genre] 13 | } 14 | 15 | struct Genre: Codable { 16 | let id: Int 17 | let name: String 18 | } 19 | -------------------------------------------------------------------------------- /SwiftUI-SDUI.xcodeproj/xcuserdata/porter.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SwiftUI-SDUI.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SwiftUI-SDUI/repository/api_models/TvShowsResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TvShowsResult.swift 3 | // SwiftUI-SDUI 4 | // 5 | // Created by porter on 07/05/20. 6 | // Copyright © 2020 kinley. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct TvShowsResult: Codable { 12 | var results: [TvShow] 13 | var title: String? 14 | } 15 | 16 | struct TvShow: Codable { 17 | var id: CLong 18 | var name: String 19 | var overview: String 20 | var poster_path: String 21 | } 22 | -------------------------------------------------------------------------------- /SwiftUI-SDUI/repository/api_models/MoviesResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoviesResult.swift 3 | // SwiftUI-SDUI 4 | // 5 | // Created by porter on 07/05/20. 6 | // Copyright © 2020 kinley. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct MoviesResult: Codable { 12 | var results: [Movie] 13 | let title: String? 14 | } 15 | 16 | 17 | struct Movie: Codable, Hashable { 18 | var id: CLong 19 | var title: String 20 | var overview: String 21 | var poster_path: String 22 | } 23 | -------------------------------------------------------------------------------- /SwiftUI-SDUI/repository/api_models/LanguageResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LanguageResult.swift 3 | // SwiftUI-SDUI 4 | // 5 | // Created by porter on 07/05/20. 6 | // Copyright © 2020 kinley. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct Language : Codable { 13 | let id: String 14 | let englishName: String 15 | let name: String 16 | 17 | private enum CodingKeys: String, CodingKey { 18 | case id = "iso_639_1" 19 | case englishName = "english_name" 20 | case name 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SwiftUI-SDUI/UIComponents/UIComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIComponent.swift 3 | // SwiftUI-SDUI 4 | // 5 | // Created by porter on 07/05/20. 6 | // Copyright © 2020 kinley. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | 13 | protocol UIDelegate {} 14 | 15 | protocol UIComponent { 16 | var uniqueId: String { get } 17 | func render(uiDelegate: UIDelegate) -> AnyView 18 | } 19 | 20 | extension View { 21 | func toAny() -> AnyView { 22 | return AnyView(self) 23 | } 24 | } 25 | 26 | 27 | func renderPage(ui: [UIComponent], uiDelegate: UIDelegate) -> AnyView { 28 | return 29 | ScrollView(.vertical) { 30 | VStack { 31 | HStack { 32 | Spacer() 33 | } 34 | ForEach(ui, id: \.uniqueId) { uiComponent in 35 | uiComponent.render(uiDelegate: uiDelegate) 36 | } 37 | .transition(AnyTransition.scale) 38 | Spacer() 39 | } 40 | 41 | }.toAny() 42 | } 43 | -------------------------------------------------------------------------------- /SwiftUI-SDUI/HomePageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // SwiftUI-SDUI 4 | // 5 | // Created by porter on 07/05/20. 6 | // Copyright © 2020 kinley. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct HomePageView: View, UIDelegate, NotificationDelegate { 12 | 13 | @ObservedObject 14 | var controller: HomePageController 15 | 16 | var body: some View { 17 | 18 | renderPage(ui: controller.uiComponents, uiDelegate: self) 19 | .onAppear(perform: { 20 | self.controller.loadPage() 21 | }) 22 | .background(SwiftUI.Color.white.edgesIgnoringSafeArea(.all)) 23 | } 24 | 25 | func cancelClick(identifier: String) { 26 | withAnimation { 27 | self.controller.removeComponent(id: identifier) 28 | } 29 | 30 | } 31 | 32 | func actionClick() { 33 | 34 | } 35 | } 36 | 37 | struct ContentView_Previews: PreviewProvider { 38 | static var previews: some View { 39 | HomePageView(controller: HomePageController()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /SwiftUI-SDUI/repository/TmDbRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TmDbRepository.swift 3 | // SwiftUI-SDUI 4 | // 5 | // Created by porter on 07/05/20. 6 | // Copyright © 2020 kinley. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | import Alamofire 12 | 13 | let apiKey = "api_key=02e40a2424558958a9d91847362b03ae" 14 | 15 | protocol TmdbRepository { 16 | 17 | func getPopularMovies() -> Observable 18 | 19 | func getPopularTvShows() -> Observable 20 | 21 | func getGenreList() -> Observable 22 | 23 | func getLanguages() -> Observable<[Language]> 24 | } 25 | 26 | class TmDbRepositoryImpl : BaseRepository, TmdbRepository { 27 | 28 | func getPopularMovies() -> Observable { 29 | return super.createRequest(url: "https://api.themoviedb.org/3/movie/popular?\(apiKey)") 30 | } 31 | 32 | func getPopularTvShows() -> Observable { 33 | super.createRequest(url: "https://api.themoviedb.org/3/tv/popular?\(apiKey)") 34 | } 35 | 36 | func getGenreList() -> Observable { 37 | return super.createRequest(url: "https://api.themoviedb.org/3/genre/movie/list?\(apiKey)") 38 | } 39 | 40 | func getLanguages() -> Observable<[Language]> { 41 | return super.createRequest(url: "https://api.themoviedb.org/3/configuration/languages?\(apiKey)") 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /SwiftUI-SDUI/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SwiftUI-SDUI 4 | // 5 | // Created by porter on 07/05/20. 6 | // Copyright © 2020 kinley. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /SwiftUI-SDUI/UIComponents/GenreListComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenreListComponent.swift 3 | // SwiftUI-SDUI 4 | // 5 | // Created by porter on 07/05/20. 6 | // Copyright © 2020 kinley. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | struct GenreListComponent: UIComponent { 13 | let genres: [Genre] 14 | var uniqueId: String 15 | 16 | func render(uiDelegate: UIDelegate) -> AnyView { 17 | GenreListView(genres: genres).toAny() 18 | } 19 | 20 | } 21 | 22 | struct GenreListView: View { 23 | let genres: [Genre] 24 | 25 | var body: some View { 26 | VStack { 27 | HStack { 28 | Text("Genres") 29 | Spacer() 30 | } 31 | ScrollView(.horizontal, showsIndicators: false) { 32 | HStack { 33 | ForEach(genres, id: \.id) { genre in 34 | GenreView(genre: genre) 35 | } 36 | } 37 | } 38 | } 39 | .padding() 40 | } 41 | } 42 | 43 | struct GenreView: View { 44 | 45 | let genre: Genre 46 | 47 | var body: some View { 48 | 49 | Text(genre.name) 50 | .foregroundColor(Color.white) 51 | .padding(.all) 52 | .background(Color.gray) 53 | .border(/*@START_MENU_TOKEN@*/Color.gray/*@END_MENU_TOKEN@*/, width: 2) 54 | } 55 | } 56 | 57 | 58 | struct Genre_Preview: PreviewProvider { 59 | static var previews: some View { 60 | GenreView(genre: Genre(id: 12, name: "Action")) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /SwiftUI-SDUI/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 | -------------------------------------------------------------------------------- /SwiftUI-SDUI/repository/BaseRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseRepository.swift 3 | // SwiftUI-SDUI 4 | // 5 | // Created by porter on 07/05/20. 6 | // Copyright © 2020 kinley. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | import Alamofire 12 | 13 | class BaseRepository { 14 | func createRequest(url: String) -> Observable { 15 | 16 | let observable = Observable.create { observer -> Disposable in 17 | 18 | Alamofire.request(url) 19 | .validate() 20 | .responseJSON { response in 21 | switch response.result { 22 | case .success: 23 | guard let data = response.data else { 24 | // if no error provided by alamofire return .notFound error instead. 25 | // .notFound should never happen here? 26 | observer.onError(response.error ?? AppError.runtimeError("random message")) 27 | return 28 | } 29 | do { 30 | let projects = try JSONDecoder().decode(T.self, from: data) 31 | observer.onNext(projects) 32 | } catch { 33 | observer.onError(error) 34 | } 35 | case .failure(let error): 36 | observer.onError(error) 37 | } 38 | } 39 | return Disposables.create() 40 | } 41 | observable 42 | .observeOn(MainScheduler.instance) 43 | 44 | return observable 45 | } 46 | } 47 | 48 | enum AppError: Error { 49 | case runtimeError(String) 50 | } 51 | -------------------------------------------------------------------------------- /SwiftUI-SDUI/UIComponents/LanguageListComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LanguageListComponent.swift 3 | // SwiftUI-SDUI 4 | // 5 | // Created by porter on 07/05/20. 6 | // Copyright © 2020 kinley. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | 13 | struct LanguageListComponent : UIComponent { 14 | 15 | let languages: [Language] 16 | var uniqueId: String = "" 17 | 18 | func render(uiDelegate: UIDelegate) -> AnyView { 19 | LanguageListView(languages: languages).toAny() 20 | } 21 | 22 | } 23 | 24 | struct LanguageListView: View { 25 | 26 | let languages: [Language] 27 | 28 | var body: some View { 29 | VStack { 30 | HStack { 31 | Text("Languages") 32 | Spacer() 33 | } 34 | 35 | ScrollView(.horizontal, showsIndicators: false) { 36 | HStack { 37 | ForEach(languages, id: \.id) { language in 38 | LanguageView(language: language) 39 | } 40 | } 41 | } 42 | } 43 | .padding() 44 | } 45 | } 46 | 47 | struct LanguageView: View { 48 | let language: Language 49 | var body: some View { 50 | VStack { 51 | Text(language.englishName) 52 | .font(.subheadline) 53 | .foregroundColor(Color.white) 54 | .padding(.all) 55 | .background(Color.gray) 56 | .border(/*@START_MENU_TOKEN@*/Color.gray/*@END_MENU_TOKEN@*/, width: 2) 57 | 58 | } 59 | } 60 | } 61 | 62 | struct LanguageView_Preview: PreviewProvider { 63 | static var previews: some View { 64 | LanguageView(language: Language(id: "en", englishName: "English", name: "English")) 65 | .background(SwiftUI.Color.gray.edgesIgnoringSafeArea(.all)) 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /SwiftUI-SDUI/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /SwiftUI-SDUI/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 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /SwiftUI-SDUI/HomePage/HomePageController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomePageController.swift 3 | // SwiftUI-SDUI 4 | // 5 | // Created by porter on 07/05/20. 6 | // Copyright © 2020 kinley. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | 12 | 13 | class HomePageController: ObservableObject { 14 | 15 | @Published 16 | var uiComponents: [UIComponent] = [] 17 | 18 | let disposableBag = DisposeBag() 19 | 20 | let repository: TmdbRepository = TmDbRepositoryImpl() 21 | 22 | func loadPage() { 23 | uiComponents = [] 24 | Observable 25 | .zip(repository.getPopularMovies(), repository.getPopularTvShows(), repository.getGenreList(), repository.getLanguages(), 26 | resultSelector: { (movieResult, tvShowsResult, genresResult, languages) in 27 | var components: [UIComponent] = [] 28 | 29 | components.append(NotificationComponent(uniqueId: "Subsciption", uiModel: NotificationUIModel(header: "Subsciption", message: "Your subscription has expired", actionText: "Renew"))) 30 | components.append(MovieListUIComponent(movieResult: MoviesResult(results: movieResult.results, title: "Popular Movies"))) 31 | components.append(TvShowsListUIComponent(tvShowsResult: TvShowsResult(results: tvShowsResult.results, title: "Popular Tv Shows"))) 32 | components.append(GenreListComponent(genres: genresResult.genres, uniqueId: "Genre")) 33 | components.append(LanguageListComponent(languages: languages, uniqueId: "Languages")) 34 | 35 | return components 36 | }) 37 | .subscribe( 38 | onNext: { [weak self] components in 39 | self?.uiComponents = components 40 | }, 41 | onError: { error in 42 | debugPrint(error) 43 | } 44 | ) 45 | .disposed(by: disposableBag) 46 | 47 | } 48 | 49 | 50 | // Removes the subscriptin component 51 | func removeComponent(id: String) { 52 | uiComponents = uiComponents.filter() { component in 53 | component.uniqueId != id 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /SwiftUI-SDUI/UIComponents/MovieListComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieListComponent.swift 3 | // SwiftUI-SDUI 4 | // 5 | // Created by porter on 07/05/20. 6 | // Copyright © 2020 kinley. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import KingfisherSwiftUI 12 | 13 | 14 | class MovieListUIComponent : UIComponent { 15 | let movieResult: MoviesResult 16 | var uniqueId: String 17 | 18 | init(movieResult: MoviesResult) { 19 | self.movieResult = movieResult 20 | self.uniqueId = movieResult.title ?? "Movies" 21 | } 22 | 23 | func render(uiDelegate: UIDelegate) -> AnyView { 24 | return MovieListView(movieResult: movieResult).toAny() 25 | } 26 | } 27 | 28 | struct MovieListView: View { 29 | let movieResult: MoviesResult 30 | 31 | var body: some View { 32 | VStack { 33 | HStack { 34 | Text(movieResult.title ?? "Movies" ) 35 | .font(.headline) 36 | 37 | Spacer() 38 | } 39 | ScrollView(.horizontal, showsIndicators: false) { 40 | HStack { 41 | ForEach(movieResult.results, id: \.title) { movie in 42 | MovieView(movie: movie) 43 | } 44 | } 45 | } 46 | } 47 | .padding() 48 | 49 | } 50 | } 51 | 52 | struct MovieView: View { 53 | let movie: Movie 54 | 55 | var body: some View { 56 | VStack { 57 | 58 | KFImage(URL(string: "https://image.tmdb.org/t/p/w300/\(movie.poster_path)")) 59 | .resizable() 60 | .frame(width: 180, height: 270) 61 | .cornerRadius(20) 62 | 63 | Text(movie.title) 64 | .frame(width: 180, alignment: Alignment.center) 65 | .foregroundColor(.black) 66 | .lineLimit(1) 67 | 68 | } 69 | 70 | } 71 | } 72 | 73 | 74 | struct MovieView_Previews: PreviewProvider { 75 | static var previews: some View { 76 | MovieView(movie: Movie(id: 12, title: "DDLJ", overview: "Boring movie", poster_path: "")) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /SwiftUI-SDUI/UIComponents/TvShowsListComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TvShowsListComponent.swift 3 | // SwiftUI-SDUI 4 | // 5 | // Created by porter on 07/05/20. 6 | // Copyright © 2020 kinley. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import KingfisherSwiftUI 12 | 13 | class TvShowsListUIComponent : UIComponent { 14 | let tvShowsResult: TvShowsResult 15 | var uniqueId: String 16 | 17 | init(tvShowsResult: TvShowsResult) { 18 | self.tvShowsResult = tvShowsResult 19 | self.uniqueId = tvShowsResult.title ?? "shows" 20 | } 21 | 22 | func render(uiDelegate: UIDelegate) -> AnyView { 23 | return TvShowListView(tvShowsResult: tvShowsResult).toAny() 24 | } 25 | } 26 | 27 | struct TvShowListView: View { 28 | 29 | let tvShowsResult: TvShowsResult 30 | 31 | var body: some View { 32 | VStack { 33 | HStack { 34 | Text(tvShowsResult.title ?? "Popular Shows") 35 | .font(.headline) 36 | Spacer() 37 | } 38 | ScrollView(.horizontal, showsIndicators: false) { 39 | HStack { 40 | ForEach(tvShowsResult.results, id: \.name) { show in 41 | TvShowView(show: show) 42 | } 43 | } 44 | } 45 | } 46 | .padding() 47 | 48 | } 49 | } 50 | 51 | struct TvShowView: View { 52 | let show: TvShow 53 | 54 | var body: some View { 55 | VStack { 56 | 57 | KFImage(URL(string: "https://image.tmdb.org/t/p/w300/\(show.poster_path)")) 58 | .resizable() 59 | .frame(width: 180, height: 270) 60 | .cornerRadius(20) 61 | 62 | Text(show.name) 63 | .frame(width: 180, alignment: Alignment.center) 64 | .foregroundColor(.black) 65 | .lineLimit(1) 66 | 67 | } 68 | } 69 | } 70 | 71 | 72 | struct TvShowView_Previews: PreviewProvider { 73 | static var previews: some View { 74 | TvShowView(show: TvShow(id: 12, name: "DDLJ", overview: "Boring movie", poster_path: "")) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /SwiftUI-SDUI/UIComponents/SubscriptionComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SubscriptionComponent.swift 3 | // SwiftUI-SDUI 4 | // 5 | // Created by porter on 07/05/20. 6 | // Copyright © 2020 kinley. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | struct NotificationComponent: UIComponent { 13 | var uniqueId: String 14 | 15 | let uiModel: NotificationUIModel 16 | 17 | func render(uiDelegate: UIDelegate) -> AnyView { 18 | NotificationView(component: self, uiDelegate: uiDelegate as! NotificationDelegate, uiModel: uiModel).toAny() 19 | } 20 | } 21 | 22 | 23 | struct NotificationView: View { 24 | 25 | let component: NotificationComponent 26 | let uiDelegate: NotificationDelegate 27 | let uiModel: NotificationUIModel 28 | 29 | var body: some View { 30 | 31 | VStack { 32 | HStack { 33 | Text(uiModel.header) 34 | .font(.title) 35 | .frame(alignment: Alignment.leading) 36 | 37 | Spacer() 38 | 39 | Text("Cancel") 40 | .font(.headline) 41 | .foregroundColor(.red) 42 | .onTapGesture { 43 | self.uiDelegate.cancelClick(identifier: self.component.uniqueId) 44 | } 45 | } 46 | 47 | HStack { 48 | Text(uiModel.message) 49 | Spacer() 50 | } 51 | 52 | HStack { 53 | Spacer() 54 | Button(action: { 55 | self.uiDelegate.actionClick() 56 | }) { 57 | Text(uiModel.actionText) 58 | } 59 | } 60 | } 61 | .padding() 62 | .background(Color(red: 0.65, green: 0.4, blue: 0.2, opacity: 0.2)) 63 | } 64 | } 65 | 66 | struct NotificationUIModel { 67 | let header: String 68 | let message: String 69 | let actionText: String 70 | } 71 | 72 | class ProxyUIDelegate : UIDelegate {} 73 | 74 | protocol NotificationDelegate: UIDelegate { 75 | func cancelClick(identifier: String) 76 | 77 | func actionClick() 78 | } 79 | -------------------------------------------------------------------------------- /SwiftUI-SDUI/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // SwiftUI-SDUI 4 | // 5 | // Created by porter on 07/05/20. 6 | // Copyright © 2020 kinley. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 21 | 22 | // Create the SwiftUI view that provides the window contents. 23 | let contentView = HomePageView(controller: HomePageController()) 24 | 25 | // Use a UIHostingController as window root view controller. 26 | if let windowScene = scene as? UIWindowScene { 27 | let window = UIWindow(windowScene: windowScene) 28 | window.rootViewController = UIHostingController(rootView: contentView) 29 | self.window = window 30 | window.makeKeyAndVisible() 31 | } 32 | } 33 | 34 | func sceneDidDisconnect(_ scene: UIScene) { 35 | // Called as the scene is being released by the system. 36 | // This occurs shortly after the scene enters the background, or when its session is discarded. 37 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 38 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 39 | } 40 | 41 | func sceneDidBecomeActive(_ scene: UIScene) { 42 | // Called when the scene has moved from an inactive state to an active state. 43 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 44 | } 45 | 46 | func sceneWillResignActive(_ scene: UIScene) { 47 | // Called when the scene will move from an active state to an inactive state. 48 | // This may occur due to temporary interruptions (ex. an incoming phone call). 49 | } 50 | 51 | func sceneWillEnterForeground(_ scene: UIScene) { 52 | // Called as the scene transitions from the background to the foreground. 53 | // Use this method to undo the changes made on entering the background. 54 | } 55 | 56 | func sceneDidEnterBackground(_ scene: UIScene) { 57 | // Called as the scene transitions from the foreground to the background. 58 | // Use this method to save data, release shared resources, and store enough scene-specific state information 59 | // to restore the scene back to its current state. 60 | } 61 | 62 | 63 | } 64 | 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftUI-Server-Driven-UI 2 | 3 | Please go to this __[Server-Driven-UI Architecture using UIComponents](https://medium.com/better-programming/build-a-server-driven-ui-using-ui-components-in-swiftui-466ecca97290)__ if you would like to read it on [Medium](https://medium.com/better-programming/build-a-server-driven-ui-using-ui-components-in-swiftui-466ecca97290) platform 4 | 5 | This article will talk about 6 | * Server-Driven UI, 7 | * Its implementation using re-usable components called `UIComponents`, and 8 | * Creating a generic vertical list view for rendering UI components. 9 | * tIt will conclude with a brief discussion of how UI components can serve different purposes. 10 | 11 | ## What Is Server-Driven UI? ## 12 | * It is an architecture where the server decides the UI views that need to be rendered on the application screen. 13 | * There exists a contract between the application and the server. The basis of this __contract__ gives the server control over the UI of the application. 14 | 15 | 16 | #### Contract 🤔 17 | * The server defines the list of components. 18 | * For each of the components defined at the server, we have a corresponding UI implementation in the app (UIComponent). 19 | 20 | 21 | Consider an entertainment app like Hotstar, whose contract is defined as shown below. On the left are the components from the server(__ServerComponent__), and on the right are the corresponding __UI Components__. 22 | 23 | ![](https://miro.medium.com/max/1400/1*e0caqOJanQdl7yvrU1Y0pg.png) 24 | 25 | #### Working #### 26 | * The screen does not have a predefined layout like a storyboard. Rather, it consists of a generic list view rendering multiple different views vertically, as per the server response. 27 | * To make it possible, we have to create views that are standalone and can be reused throughout the application. We call these re-usable views the UIComponent. 28 | 29 | #### Contract #### 30 | > For every ServerComponent, we have a corresponding UIComponent. 31 | 32 | 33 | ### SwiftUI ### 34 | Swift is a UI toolkit that lets you design application screens in a programmatic, declarative way. 35 | ```swift 36 | struct NotificationView: View { 37 | let notificationMessage: String 38 | 39 | var body: some View { 40 | Text(notificationMessage) 41 | } 42 | } 43 | ``` 44 | 45 | ## Server-Driven UI Implementation in SwiftUI ## 46 | 47 | This is a three-step process. 48 | 1. Define the standalone UIComponents. 49 | 2. Construct the UIComponents based on the API response. 50 | 3. Render the UIComponents on the screen. 51 | 52 | ### Step 1 - Define standalone UIComponents ### 53 | 54 | ![](https://miro.medium.com/max/1400/1*vaTfkYDRJuPnUQm8nYskgQ.png) 55 | 56 | __Input:__ Firstly, for the UIComponent to render itself, it should be provided with data.
__Output:__ UIComponent defines its UI. When used for rendering inside a screen, it renders itself based on the data (input) provided to it. 57 | 58 | 59 | #### UIComponent implementation #### 60 | 61 | ```swift 62 | protocol UIComponent { 63 | var uniqueId: String { get } 64 | func render() -> AnyView 65 | } 66 | ``` 67 | 68 | * All the UI views have to conform to this UI-component protocol. 69 | * As the components are rendered inside a generic vertical list, each UIComponent has to be independently identified. The `uniqueId` property is used to serve that purpose. 70 | * The `render()` is where the UI of the component is defined. Calling this function on a screen will render the component. 71 | 72 | Let's look at NotificationComponent. 73 | 74 | 75 | ```swift 76 | struct NotificationComponent: UIComponent { 77 | var uniqueId: String 78 | 79 | // The data required for rendering is passed as a dependency 80 | let uiModel: NotificationUIModel 81 | 82 | // Defines the View for the Component 83 | func render() -> AnyView { 84 | NotificationView(uiModel: uiModel).toAny() 85 | } 86 | } 87 | 88 | // Contains the properties required for rendering the Notification View 89 | struct NotificationUIModel { 90 | let header: String 91 | let message: String 92 | let actionText: String 93 | } 94 | 95 | // Notification view takes the NotificationUIModel as a dependency 96 | struct NotificationView: View { 97 | let uiModel: NotificationUIModel 98 | var body: some View { 99 | VStack { 100 | Text(uiModel.header) 101 | Text(uiModel.message) 102 | Button(action: {}) { 103 | Text(uiModel.actionText) 104 | } 105 | } 106 | } 107 | } 108 | ``` 109 | 110 | * `NotificationUIModel` is the data required by the component to render. This is the input to the UIComponent. 111 | * `NotificationView` is a SwiftUI view that defines the UI of the component. It takes in `NotificationUIModel` as a dependency. This view is the output of the UIComponent when used for rendering on the screen. 112 | 113 | 114 | ### Step 2 - Construct the UIComponents based on the API response ### 115 | ```swift 116 | class HomePageController: ObservableObject { 117 | 118 | let repository: Repository 119 | @Published var uiComponents: [UIComponent] = [] 120 | 121 | .. 122 | .. 123 | 124 | func loadPage() { 125 | val response = repository.getHomePageResult() 126 | response.forEach { serverComponent in 127 | let uiComponent = parseToUIComponent(serverComponent) 128 | uiComponents.append(uiComponent) 129 | } 130 | } 131 | } 132 | 133 | func parseToUIComponent(serverComponent: ServerComponent) -> UIComponent { 134 | var uiComponent: UIComponent 135 | 136 | if serverComponent.type == "NotificationComponent" { 137 | uiComponent = NotificationComponent(serverComponent.data, serverComponent.id) 138 | } 139 | else if serverComponent.type == "GenreListComponent" { 140 | uiComponent = GenreListComponent(serverComponent.data, serverComponent.id) 141 | } 142 | ... 143 | ... 144 | return uiComponent 145 | } 146 | ``` 147 | 148 | * `HomePageController` loads the server components from the repository and converts them into the UIComponents. 149 | * The `uiComponent`'s property is responsible for holding the list of UIComponents. Wrapping it with the `@Published` property makes it an observable. Any change in its value will be published to the `Observer(View)`. *__This makes it possible to keep the View in sync with the state of the application.__* 150 | 151 | 152 | ### Step 3 - Render UIComponents on the screen using Generic List ### 153 | This the last part. The screen’s only responsibility is to render the `UIComponents`. 154 | * It subscribes to the `uiComponents` observable. 155 | * Whenever the value of the `uiComponents` changes, the `HomePage` is notified, which then updates its UI. 156 | * A generic ListView is used for rendering the UIComponents 157 | 158 | ```swift 159 | struct HomePageView: View { 160 | 161 | @ObservedObject var controller: HomePageViewModel 162 | 163 | var body: some View { 164 | 165 | ScrollView(.vertical) { 166 | VStack { 167 | ForEach(controller.uiComponents, id: \.uniqueId) { uiComponent in 168 | uiComponent.render() 169 | } 170 | } 171 | } 172 | .onAppear(perform: { 173 | self.controller.loadPage() 174 | }) 175 | 176 | } 177 | } 178 | ``` 179 | 180 | #### Rendering using Generic Vstack #### 181 | All the UIComponents are rendered vertically using a VStack inside. As the UIComponents are uniquely identifiable, we can use the `ForEach` construct for rendering. 182 | 183 | Since all the components conforming to UIComponent protocol must return a common type, *__the render() function returns AnyView__*. Below is an extension on the View for converting it toAnyView. 184 | ```swift 185 | extension View { 186 | func toAny() -> AnyView { 187 | return AnyView(self) 188 | } 189 | } 190 | ``` 191 | 192 | ## Conclusion ## 193 | We saw how `UIComponent` can be used to give the server control over the UI of the application. But with UIComponents you can achieve something more. 194 | 195 | Let’s consider a case without server-driven UI. It's often the case that the pieces of UI are used many times across the application. This leads to duplication of the view and view logic. So, it’s better to divide the UI into meaningful, reusable UI-components. 196 | 197 | Having them this way will let the domain-layer/business layer define and construct the UI components. Additionally, the business-layer can take the responsibility of controlling the UI. 198 | 199 | Have a look at the article __[Android Jetpack Compose — Create a Component-Based Architecture](https://medium.com/better-programming/create-a-component-based-architecture-in-android-jetpack-compose-96980c191351)__, which explains UI-Components in detail. As it uses Jetpack compose-Android’s declarative UI kit, it wouldn’t be hard to understand. 200 | 201 | 202 | -------------------------------------------------------------------------------- /SwiftUI-SDUI.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 6144C1172464732B006E9EEA /* SubscriptionComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6144C1162464732B006E9EEA /* SubscriptionComponent.swift */; }; 11 | 61F5E9EC2464556700E6956E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F5E9EB2464556700E6956E /* AppDelegate.swift */; }; 12 | 61F5E9EE2464556700E6956E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F5E9ED2464556700E6956E /* SceneDelegate.swift */; }; 13 | 61F5E9F02464556700E6956E /* HomePageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F5E9EF2464556700E6956E /* HomePageView.swift */; }; 14 | 61F5E9F22464556800E6956E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 61F5E9F12464556800E6956E /* Assets.xcassets */; }; 15 | 61F5E9F52464556800E6956E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 61F5E9F42464556800E6956E /* Preview Assets.xcassets */; }; 16 | 61F5E9F82464556800E6956E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 61F5E9F62464556800E6956E /* LaunchScreen.storyboard */; }; 17 | 61F5EA012464559100E6956E /* HomePageController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F5EA002464559100E6956E /* HomePageController.swift */; }; 18 | 61F5EA04246455D200E6956E /* UIComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F5EA03246455D200E6956E /* UIComponent.swift */; }; 19 | 61F5EA07246456F300E6956E /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 61F5EA06246456F300E6956E /* Alamofire */; }; 20 | 61F5EA0A2464579B00E6956E /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 61F5EA092464579B00E6956E /* RxSwift */; }; 21 | 61F5EA0D246457CD00E6956E /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 61F5EA0C246457CD00E6956E /* KingfisherSwiftUI */; }; 22 | 61F5EA102464585D00E6956E /* TmDbRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F5EA0F2464585D00E6956E /* TmDbRepository.swift */; }; 23 | 61F5EA12246458BC00E6956E /* BaseRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F5EA11246458BC00E6956E /* BaseRepository.swift */; }; 24 | 61F5EA152464592300E6956E /* MoviesResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F5EA142464592300E6956E /* MoviesResult.swift */; }; 25 | 61F5EA1724645AC700E6956E /* TvShowsResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F5EA1624645AC700E6956E /* TvShowsResult.swift */; }; 26 | 61F5EA1924645C1100E6956E /* GenreResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F5EA1824645C1100E6956E /* GenreResult.swift */; }; 27 | 61F5EA1B24645C2B00E6956E /* LanguageResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F5EA1A24645C2B00E6956E /* LanguageResult.swift */; }; 28 | 61F5EA1D24645CF000E6956E /* GenreListComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F5EA1C24645CF000E6956E /* GenreListComponent.swift */; }; 29 | 61F5EA1F24645D2D00E6956E /* LanguageListComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F5EA1E24645D2D00E6956E /* LanguageListComponent.swift */; }; 30 | 61F5EA2124645D9700E6956E /* MovieListComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F5EA2024645D9700E6956E /* MovieListComponent.swift */; }; 31 | 61F5EA2324645DA600E6956E /* TvShowsListComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F5EA2224645DA600E6956E /* TvShowsListComponent.swift */; }; 32 | /* End PBXBuildFile section */ 33 | 34 | /* Begin PBXFileReference section */ 35 | 6144C1162464732B006E9EEA /* SubscriptionComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionComponent.swift; sourceTree = ""; }; 36 | 61F5E9E82464556700E6956E /* SwiftUI-SDUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SwiftUI-SDUI.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | 61F5E9EB2464556700E6956E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 38 | 61F5E9ED2464556700E6956E /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 39 | 61F5E9EF2464556700E6956E /* HomePageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageView.swift; sourceTree = ""; }; 40 | 61F5E9F12464556800E6956E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 41 | 61F5E9F42464556800E6956E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 42 | 61F5E9F72464556800E6956E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 43 | 61F5E9F92464556800E6956E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 44 | 61F5EA002464559100E6956E /* HomePageController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageController.swift; sourceTree = ""; }; 45 | 61F5EA03246455D200E6956E /* UIComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIComponent.swift; sourceTree = ""; }; 46 | 61F5EA0F2464585D00E6956E /* TmDbRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TmDbRepository.swift; sourceTree = ""; }; 47 | 61F5EA11246458BC00E6956E /* BaseRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseRepository.swift; sourceTree = ""; }; 48 | 61F5EA142464592300E6956E /* MoviesResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesResult.swift; sourceTree = ""; }; 49 | 61F5EA1624645AC700E6956E /* TvShowsResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TvShowsResult.swift; sourceTree = ""; }; 50 | 61F5EA1824645C1100E6956E /* GenreResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenreResult.swift; sourceTree = ""; }; 51 | 61F5EA1A24645C2B00E6956E /* LanguageResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageResult.swift; sourceTree = ""; }; 52 | 61F5EA1C24645CF000E6956E /* GenreListComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenreListComponent.swift; sourceTree = ""; }; 53 | 61F5EA1E24645D2D00E6956E /* LanguageListComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageListComponent.swift; sourceTree = ""; }; 54 | 61F5EA2024645D9700E6956E /* MovieListComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieListComponent.swift; sourceTree = ""; }; 55 | 61F5EA2224645DA600E6956E /* TvShowsListComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TvShowsListComponent.swift; sourceTree = ""; }; 56 | /* End PBXFileReference section */ 57 | 58 | /* Begin PBXFrameworksBuildPhase section */ 59 | 61F5E9E52464556700E6956E /* Frameworks */ = { 60 | isa = PBXFrameworksBuildPhase; 61 | buildActionMask = 2147483647; 62 | files = ( 63 | 61F5EA0D246457CD00E6956E /* KingfisherSwiftUI in Frameworks */, 64 | 61F5EA0A2464579B00E6956E /* RxSwift in Frameworks */, 65 | 61F5EA07246456F300E6956E /* Alamofire in Frameworks */, 66 | ); 67 | runOnlyForDeploymentPostprocessing = 0; 68 | }; 69 | /* End PBXFrameworksBuildPhase section */ 70 | 71 | /* Begin PBXGroup section */ 72 | 61F5E9DF2464556700E6956E = { 73 | isa = PBXGroup; 74 | children = ( 75 | 61F5E9EA2464556700E6956E /* SwiftUI-SDUI */, 76 | 61F5E9E92464556700E6956E /* Products */, 77 | ); 78 | sourceTree = ""; 79 | }; 80 | 61F5E9E92464556700E6956E /* Products */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | 61F5E9E82464556700E6956E /* SwiftUI-SDUI.app */, 84 | ); 85 | name = Products; 86 | sourceTree = ""; 87 | }; 88 | 61F5E9EA2464556700E6956E /* SwiftUI-SDUI */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | 61F5EA0E2464583A00E6956E /* repository */, 92 | 61F5EA02246455C200E6956E /* UIComponents */, 93 | 61F5E9FF2464557F00E6956E /* HomePage */, 94 | 61F5E9EB2464556700E6956E /* AppDelegate.swift */, 95 | 61F5E9ED2464556700E6956E /* SceneDelegate.swift */, 96 | 61F5E9EF2464556700E6956E /* HomePageView.swift */, 97 | 61F5E9F12464556800E6956E /* Assets.xcassets */, 98 | 61F5E9F62464556800E6956E /* LaunchScreen.storyboard */, 99 | 61F5E9F92464556800E6956E /* Info.plist */, 100 | 61F5E9F32464556800E6956E /* Preview Content */, 101 | ); 102 | path = "SwiftUI-SDUI"; 103 | sourceTree = ""; 104 | }; 105 | 61F5E9F32464556800E6956E /* Preview Content */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 61F5E9F42464556800E6956E /* Preview Assets.xcassets */, 109 | ); 110 | path = "Preview Content"; 111 | sourceTree = ""; 112 | }; 113 | 61F5E9FF2464557F00E6956E /* HomePage */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 61F5EA002464559100E6956E /* HomePageController.swift */, 117 | ); 118 | path = HomePage; 119 | sourceTree = ""; 120 | }; 121 | 61F5EA02246455C200E6956E /* UIComponents */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | 61F5EA03246455D200E6956E /* UIComponent.swift */, 125 | 61F5EA1C24645CF000E6956E /* GenreListComponent.swift */, 126 | 61F5EA1E24645D2D00E6956E /* LanguageListComponent.swift */, 127 | 61F5EA2024645D9700E6956E /* MovieListComponent.swift */, 128 | 61F5EA2224645DA600E6956E /* TvShowsListComponent.swift */, 129 | 6144C1162464732B006E9EEA /* SubscriptionComponent.swift */, 130 | ); 131 | path = UIComponents; 132 | sourceTree = ""; 133 | }; 134 | 61F5EA0E2464583A00E6956E /* repository */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | 61F5EA13246458FE00E6956E /* api_models */, 138 | 61F5EA0F2464585D00E6956E /* TmDbRepository.swift */, 139 | 61F5EA11246458BC00E6956E /* BaseRepository.swift */, 140 | ); 141 | path = repository; 142 | sourceTree = ""; 143 | }; 144 | 61F5EA13246458FE00E6956E /* api_models */ = { 145 | isa = PBXGroup; 146 | children = ( 147 | 61F5EA142464592300E6956E /* MoviesResult.swift */, 148 | 61F5EA1624645AC700E6956E /* TvShowsResult.swift */, 149 | 61F5EA1824645C1100E6956E /* GenreResult.swift */, 150 | 61F5EA1A24645C2B00E6956E /* LanguageResult.swift */, 151 | ); 152 | path = api_models; 153 | sourceTree = ""; 154 | }; 155 | /* End PBXGroup section */ 156 | 157 | /* Begin PBXNativeTarget section */ 158 | 61F5E9E72464556700E6956E /* SwiftUI-SDUI */ = { 159 | isa = PBXNativeTarget; 160 | buildConfigurationList = 61F5E9FC2464556800E6956E /* Build configuration list for PBXNativeTarget "SwiftUI-SDUI" */; 161 | buildPhases = ( 162 | 61F5E9E42464556700E6956E /* Sources */, 163 | 61F5E9E52464556700E6956E /* Frameworks */, 164 | 61F5E9E62464556700E6956E /* Resources */, 165 | ); 166 | buildRules = ( 167 | ); 168 | dependencies = ( 169 | ); 170 | name = "SwiftUI-SDUI"; 171 | packageProductDependencies = ( 172 | 61F5EA06246456F300E6956E /* Alamofire */, 173 | 61F5EA092464579B00E6956E /* RxSwift */, 174 | 61F5EA0C246457CD00E6956E /* KingfisherSwiftUI */, 175 | ); 176 | productName = "SwiftUI-SDUI"; 177 | productReference = 61F5E9E82464556700E6956E /* SwiftUI-SDUI.app */; 178 | productType = "com.apple.product-type.application"; 179 | }; 180 | /* End PBXNativeTarget section */ 181 | 182 | /* Begin PBXProject section */ 183 | 61F5E9E02464556700E6956E /* Project object */ = { 184 | isa = PBXProject; 185 | attributes = { 186 | LastSwiftUpdateCheck = 1130; 187 | LastUpgradeCheck = 1130; 188 | ORGANIZATIONNAME = kinley; 189 | TargetAttributes = { 190 | 61F5E9E72464556700E6956E = { 191 | CreatedOnToolsVersion = 11.3.1; 192 | }; 193 | }; 194 | }; 195 | buildConfigurationList = 61F5E9E32464556700E6956E /* Build configuration list for PBXProject "SwiftUI-SDUI" */; 196 | compatibilityVersion = "Xcode 9.3"; 197 | developmentRegion = en; 198 | hasScannedForEncodings = 0; 199 | knownRegions = ( 200 | en, 201 | Base, 202 | ); 203 | mainGroup = 61F5E9DF2464556700E6956E; 204 | packageReferences = ( 205 | 61F5EA05246456F300E6956E /* XCRemoteSwiftPackageReference "Alamofire" */, 206 | 61F5EA082464579B00E6956E /* XCRemoteSwiftPackageReference "RxSwift" */, 207 | 61F5EA0B246457CD00E6956E /* XCRemoteSwiftPackageReference "Kingfisher" */, 208 | ); 209 | productRefGroup = 61F5E9E92464556700E6956E /* Products */; 210 | projectDirPath = ""; 211 | projectRoot = ""; 212 | targets = ( 213 | 61F5E9E72464556700E6956E /* SwiftUI-SDUI */, 214 | ); 215 | }; 216 | /* End PBXProject section */ 217 | 218 | /* Begin PBXResourcesBuildPhase section */ 219 | 61F5E9E62464556700E6956E /* Resources */ = { 220 | isa = PBXResourcesBuildPhase; 221 | buildActionMask = 2147483647; 222 | files = ( 223 | 61F5E9F82464556800E6956E /* LaunchScreen.storyboard in Resources */, 224 | 61F5E9F52464556800E6956E /* Preview Assets.xcassets in Resources */, 225 | 61F5E9F22464556800E6956E /* Assets.xcassets in Resources */, 226 | ); 227 | runOnlyForDeploymentPostprocessing = 0; 228 | }; 229 | /* End PBXResourcesBuildPhase section */ 230 | 231 | /* Begin PBXSourcesBuildPhase section */ 232 | 61F5E9E42464556700E6956E /* Sources */ = { 233 | isa = PBXSourcesBuildPhase; 234 | buildActionMask = 2147483647; 235 | files = ( 236 | 61F5E9EC2464556700E6956E /* AppDelegate.swift in Sources */, 237 | 61F5EA12246458BC00E6956E /* BaseRepository.swift in Sources */, 238 | 61F5EA012464559100E6956E /* HomePageController.swift in Sources */, 239 | 61F5E9EE2464556700E6956E /* SceneDelegate.swift in Sources */, 240 | 61F5EA2324645DA600E6956E /* TvShowsListComponent.swift in Sources */, 241 | 61F5EA2124645D9700E6956E /* MovieListComponent.swift in Sources */, 242 | 61F5EA1924645C1100E6956E /* GenreResult.swift in Sources */, 243 | 61F5E9F02464556700E6956E /* HomePageView.swift in Sources */, 244 | 61F5EA102464585D00E6956E /* TmDbRepository.swift in Sources */, 245 | 61F5EA1F24645D2D00E6956E /* LanguageListComponent.swift in Sources */, 246 | 61F5EA1D24645CF000E6956E /* GenreListComponent.swift in Sources */, 247 | 61F5EA04246455D200E6956E /* UIComponent.swift in Sources */, 248 | 61F5EA152464592300E6956E /* MoviesResult.swift in Sources */, 249 | 61F5EA1B24645C2B00E6956E /* LanguageResult.swift in Sources */, 250 | 6144C1172464732B006E9EEA /* SubscriptionComponent.swift in Sources */, 251 | 61F5EA1724645AC700E6956E /* TvShowsResult.swift in Sources */, 252 | ); 253 | runOnlyForDeploymentPostprocessing = 0; 254 | }; 255 | /* End PBXSourcesBuildPhase section */ 256 | 257 | /* Begin PBXVariantGroup section */ 258 | 61F5E9F62464556800E6956E /* LaunchScreen.storyboard */ = { 259 | isa = PBXVariantGroup; 260 | children = ( 261 | 61F5E9F72464556800E6956E /* Base */, 262 | ); 263 | name = LaunchScreen.storyboard; 264 | sourceTree = ""; 265 | }; 266 | /* End PBXVariantGroup section */ 267 | 268 | /* Begin XCBuildConfiguration section */ 269 | 61F5E9FA2464556800E6956E /* Debug */ = { 270 | isa = XCBuildConfiguration; 271 | buildSettings = { 272 | ALWAYS_SEARCH_USER_PATHS = NO; 273 | CLANG_ANALYZER_NONNULL = YES; 274 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 275 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 276 | CLANG_CXX_LIBRARY = "libc++"; 277 | CLANG_ENABLE_MODULES = YES; 278 | CLANG_ENABLE_OBJC_ARC = YES; 279 | CLANG_ENABLE_OBJC_WEAK = YES; 280 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 281 | CLANG_WARN_BOOL_CONVERSION = YES; 282 | CLANG_WARN_COMMA = YES; 283 | CLANG_WARN_CONSTANT_CONVERSION = YES; 284 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 285 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 286 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 287 | CLANG_WARN_EMPTY_BODY = YES; 288 | CLANG_WARN_ENUM_CONVERSION = YES; 289 | CLANG_WARN_INFINITE_RECURSION = YES; 290 | CLANG_WARN_INT_CONVERSION = YES; 291 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 292 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 293 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 294 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 295 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 296 | CLANG_WARN_STRICT_PROTOTYPES = YES; 297 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 298 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 299 | CLANG_WARN_UNREACHABLE_CODE = YES; 300 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 301 | COPY_PHASE_STRIP = NO; 302 | DEBUG_INFORMATION_FORMAT = dwarf; 303 | ENABLE_STRICT_OBJC_MSGSEND = YES; 304 | ENABLE_TESTABILITY = YES; 305 | GCC_C_LANGUAGE_STANDARD = gnu11; 306 | GCC_DYNAMIC_NO_PIC = NO; 307 | GCC_NO_COMMON_BLOCKS = YES; 308 | GCC_OPTIMIZATION_LEVEL = 0; 309 | GCC_PREPROCESSOR_DEFINITIONS = ( 310 | "DEBUG=1", 311 | "$(inherited)", 312 | ); 313 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 314 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 315 | GCC_WARN_UNDECLARED_SELECTOR = YES; 316 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 317 | GCC_WARN_UNUSED_FUNCTION = YES; 318 | GCC_WARN_UNUSED_VARIABLE = YES; 319 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 320 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 321 | MTL_FAST_MATH = YES; 322 | ONLY_ACTIVE_ARCH = YES; 323 | SDKROOT = iphoneos; 324 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 325 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 326 | }; 327 | name = Debug; 328 | }; 329 | 61F5E9FB2464556800E6956E /* Release */ = { 330 | isa = XCBuildConfiguration; 331 | buildSettings = { 332 | ALWAYS_SEARCH_USER_PATHS = NO; 333 | CLANG_ANALYZER_NONNULL = YES; 334 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 335 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 336 | CLANG_CXX_LIBRARY = "libc++"; 337 | CLANG_ENABLE_MODULES = YES; 338 | CLANG_ENABLE_OBJC_ARC = YES; 339 | CLANG_ENABLE_OBJC_WEAK = YES; 340 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 341 | CLANG_WARN_BOOL_CONVERSION = YES; 342 | CLANG_WARN_COMMA = YES; 343 | CLANG_WARN_CONSTANT_CONVERSION = YES; 344 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 345 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 346 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 347 | CLANG_WARN_EMPTY_BODY = YES; 348 | CLANG_WARN_ENUM_CONVERSION = YES; 349 | CLANG_WARN_INFINITE_RECURSION = YES; 350 | CLANG_WARN_INT_CONVERSION = YES; 351 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 352 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 353 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 354 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 355 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 356 | CLANG_WARN_STRICT_PROTOTYPES = YES; 357 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 358 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 359 | CLANG_WARN_UNREACHABLE_CODE = YES; 360 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 361 | COPY_PHASE_STRIP = NO; 362 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 363 | ENABLE_NS_ASSERTIONS = NO; 364 | ENABLE_STRICT_OBJC_MSGSEND = YES; 365 | GCC_C_LANGUAGE_STANDARD = gnu11; 366 | GCC_NO_COMMON_BLOCKS = YES; 367 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 368 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 369 | GCC_WARN_UNDECLARED_SELECTOR = YES; 370 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 371 | GCC_WARN_UNUSED_FUNCTION = YES; 372 | GCC_WARN_UNUSED_VARIABLE = YES; 373 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 374 | MTL_ENABLE_DEBUG_INFO = NO; 375 | MTL_FAST_MATH = YES; 376 | SDKROOT = iphoneos; 377 | SWIFT_COMPILATION_MODE = wholemodule; 378 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 379 | VALIDATE_PRODUCT = YES; 380 | }; 381 | name = Release; 382 | }; 383 | 61F5E9FD2464556800E6956E /* Debug */ = { 384 | isa = XCBuildConfiguration; 385 | buildSettings = { 386 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 387 | CODE_SIGN_STYLE = Automatic; 388 | DEVELOPMENT_ASSET_PATHS = "\"SwiftUI-SDUI/Preview Content\""; 389 | DEVELOPMENT_TEAM = 3DFC4389QS; 390 | ENABLE_PREVIEWS = YES; 391 | INFOPLIST_FILE = "SwiftUI-SDUI/Info.plist"; 392 | LD_RUNPATH_SEARCH_PATHS = ( 393 | "$(inherited)", 394 | "@executable_path/Frameworks", 395 | ); 396 | PRODUCT_BUNDLE_IDENTIFIER = "kinley.SwiftUI-SDUI"; 397 | PRODUCT_NAME = "$(TARGET_NAME)"; 398 | SWIFT_VERSION = 5.0; 399 | TARGETED_DEVICE_FAMILY = "1,2"; 400 | }; 401 | name = Debug; 402 | }; 403 | 61F5E9FE2464556800E6956E /* Release */ = { 404 | isa = XCBuildConfiguration; 405 | buildSettings = { 406 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 407 | CODE_SIGN_STYLE = Automatic; 408 | DEVELOPMENT_ASSET_PATHS = "\"SwiftUI-SDUI/Preview Content\""; 409 | DEVELOPMENT_TEAM = 3DFC4389QS; 410 | ENABLE_PREVIEWS = YES; 411 | INFOPLIST_FILE = "SwiftUI-SDUI/Info.plist"; 412 | LD_RUNPATH_SEARCH_PATHS = ( 413 | "$(inherited)", 414 | "@executable_path/Frameworks", 415 | ); 416 | PRODUCT_BUNDLE_IDENTIFIER = "kinley.SwiftUI-SDUI"; 417 | PRODUCT_NAME = "$(TARGET_NAME)"; 418 | SWIFT_VERSION = 5.0; 419 | TARGETED_DEVICE_FAMILY = "1,2"; 420 | }; 421 | name = Release; 422 | }; 423 | /* End XCBuildConfiguration section */ 424 | 425 | /* Begin XCConfigurationList section */ 426 | 61F5E9E32464556700E6956E /* Build configuration list for PBXProject "SwiftUI-SDUI" */ = { 427 | isa = XCConfigurationList; 428 | buildConfigurations = ( 429 | 61F5E9FA2464556800E6956E /* Debug */, 430 | 61F5E9FB2464556800E6956E /* Release */, 431 | ); 432 | defaultConfigurationIsVisible = 0; 433 | defaultConfigurationName = Release; 434 | }; 435 | 61F5E9FC2464556800E6956E /* Build configuration list for PBXNativeTarget "SwiftUI-SDUI" */ = { 436 | isa = XCConfigurationList; 437 | buildConfigurations = ( 438 | 61F5E9FD2464556800E6956E /* Debug */, 439 | 61F5E9FE2464556800E6956E /* Release */, 440 | ); 441 | defaultConfigurationIsVisible = 0; 442 | defaultConfigurationName = Release; 443 | }; 444 | /* End XCConfigurationList section */ 445 | 446 | /* Begin XCRemoteSwiftPackageReference section */ 447 | 61F5EA05246456F300E6956E /* XCRemoteSwiftPackageReference "Alamofire" */ = { 448 | isa = XCRemoteSwiftPackageReference; 449 | repositoryURL = "https://github.com/Alamofire/Alamofire"; 450 | requirement = { 451 | kind = upToNextMajorVersion; 452 | minimumVersion = 4.9.1; 453 | }; 454 | }; 455 | 61F5EA082464579B00E6956E /* XCRemoteSwiftPackageReference "RxSwift" */ = { 456 | isa = XCRemoteSwiftPackageReference; 457 | repositoryURL = "https://github.com/ReactiveX/RxSwift"; 458 | requirement = { 459 | kind = upToNextMajorVersion; 460 | minimumVersion = 5.1.1; 461 | }; 462 | }; 463 | 61F5EA0B246457CD00E6956E /* XCRemoteSwiftPackageReference "Kingfisher" */ = { 464 | isa = XCRemoteSwiftPackageReference; 465 | repositoryURL = "https://github.com/onevcat/Kingfisher"; 466 | requirement = { 467 | kind = upToNextMajorVersion; 468 | minimumVersion = 5.13.4; 469 | }; 470 | }; 471 | /* End XCRemoteSwiftPackageReference section */ 472 | 473 | /* Begin XCSwiftPackageProductDependency section */ 474 | 61F5EA06246456F300E6956E /* Alamofire */ = { 475 | isa = XCSwiftPackageProductDependency; 476 | package = 61F5EA05246456F300E6956E /* XCRemoteSwiftPackageReference "Alamofire" */; 477 | productName = Alamofire; 478 | }; 479 | 61F5EA092464579B00E6956E /* RxSwift */ = { 480 | isa = XCSwiftPackageProductDependency; 481 | package = 61F5EA082464579B00E6956E /* XCRemoteSwiftPackageReference "RxSwift" */; 482 | productName = RxSwift; 483 | }; 484 | 61F5EA0C246457CD00E6956E /* KingfisherSwiftUI */ = { 485 | isa = XCSwiftPackageProductDependency; 486 | package = 61F5EA0B246457CD00E6956E /* XCRemoteSwiftPackageReference "Kingfisher" */; 487 | productName = KingfisherSwiftUI; 488 | }; 489 | /* End XCSwiftPackageProductDependency section */ 490 | }; 491 | rootObject = 61F5E9E02464556700E6956E /* Project object */; 492 | } 493 | --------------------------------------------------------------------------------