├── 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 | 
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 | 
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 |
--------------------------------------------------------------------------------