├── .gitattributes ├── SwiftUI-MVVM-Example ├── Assets.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── Slide 4_3 - 1.png │ │ └── Contents.json │ └── AccentColor.colorset │ │ └── Contents.json ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Models │ ├── Role.swift │ ├── Genres.swift │ ├── MediaVideos.swift │ ├── MediaList.swift │ ├── MediaVideoList.swift │ ├── ActorList.swift │ ├── Networks.swift │ ├── Actor.swift │ └── Media.swift ├── Utils │ ├── View + Ext.swift │ ├── Double + Ext.swift │ ├── LazyNavigate.swift │ ├── Data + Ext.swift │ ├── CardProtocol.swift │ ├── AppConstants.swift │ └── RequestAbleMovieListProtocol.swift ├── Core │ └── SwiftUI_MVVM_ExampleApp.swift ├── Components │ ├── CustomSectionView.swift │ ├── HeaderCarouselMediaList.swift │ ├── LogoImageView.swift │ ├── YoutubeVideoView.swift │ ├── AnimatedAsyncImageView.swift │ ├── ActorCardView.swift │ ├── MediaListSection.swift │ ├── MediaCardView.swift │ └── SearchMovieCardView.swift ├── Modules │ ├── Tabbar │ │ ├── TabbarRoot.swift │ │ └── TabbarButton.swift │ ├── Search │ │ ├── SearchView.swift │ │ └── SearchViewModel.swift │ ├── Home │ │ ├── HomeView.swift │ │ └── HomeViewModel.swift │ └── Detail │ │ ├── DetailViewModel.swift │ │ └── DetailView.swift └── Network │ ├── NetworkManager.swift │ └── Mocks │ └── NetworkManagerMock.swift ├── SwiftUI-MVVM-Example.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── xcshareddata │ └── xcschemes │ │ └── SwiftUI-MVVM-Example.xcscheme └── project.pbxproj ├── README.md ├── LICENSE ├── SwiftUI-MVVM-ExampleTests ├── MedExp_DetailViewModelTests.swift ├── MedExp_HomeViewModelTests.swift └── Med_Exp_SearchViewModelTests.swift └── .gitignore /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Assets.xcassets/AppIcon.appiconset/Slide 4_3 - 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devmehmetates/MedExp-MVVM/HEAD/SwiftUI-MVVM-Example/Assets.xcassets/AppIcon.appiconset/Slide 4_3 - 1.png -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Models/Role.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Role.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 11.11.2022. 6 | // 7 | 8 | struct Role: Codable { 9 | private let character: String? 10 | 11 | var roleName: String { character ?? "" } 12 | } 13 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Models/Genres.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Genres.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 10.11.2022. 6 | // 7 | 8 | struct Genres: Codable, Identifiable { 9 | let id: Int 10 | private let name: String? 11 | 12 | var genreName: String { name ?? "" } 13 | } 14 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Models/MediaVideos.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaVideos.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 11.11.2022. 6 | // 7 | 8 | struct MediaVideos: Codable, Identifiable { 9 | let id: String 10 | private let key: String? 11 | 12 | var videoLink: String { key ?? "" } 13 | } 14 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Utils/View + Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View + Ext.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 9.11.2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | var loadingState: some View { 12 | VStack { 13 | ProgressView() 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Slide 4_3 - 1.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Models/MediaList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaList.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 6.11.2022. 6 | // 7 | 8 | struct MediaList: Codable { 9 | private let results: [Media]? 10 | var mediaList: [Media] { results ?? [] } 11 | 12 | enum CodingKeys: String, CodingKey { 13 | case results 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Core/SwiftUI_MVVM_ExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUI_MVVM_ExampleApp.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 1.11.2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct SwiftUI_MVVM_ExampleApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | TabbarRoot() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Models/MediaVideoList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaVideoList.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 11.11.2022. 6 | // 7 | 8 | struct MediaVideoList: Codable, Identifiable { 9 | let id: Double 10 | private let results: [MediaVideos?]? 11 | 12 | var mediaVideoList: [MediaVideos] { results?.compactMap{ $0 } ?? [] } 13 | } 14 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Utils/Double + Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Double + Ext.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 3.11.2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Double { 11 | var responsizeW: Double { return UIScreen.main.bounds.size.width * self / 100 } 12 | var responsizeH: Double { return UIScreen.main.bounds.size.height * self / 100 } 13 | } 14 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Models/ActorList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActorList.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 11.11.2022. 6 | // 7 | 8 | struct ActorList: Codable, Identifiable { 9 | let id: Int 10 | private let cast: [Actor?]? 11 | 12 | var actorList: [Actor] { (cast ?? []).compactMap{ $0 }.filter{ $0.actorOrder <= 10 }.sorted { $0.actorOrder < $1.actorOrder } } 13 | } 14 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Utils/LazyNavigate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LazyNavigate.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 13.11.2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LazyNavigate: View { 11 | let build: () -> Content 12 | init(_ build: @autoclosure @escaping () -> Content) { 13 | self.build = build 14 | } 15 | var body: Content { 16 | build() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Utils/Data + Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data + Ext.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 8.11.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Data { 11 | func decodedModel() -> T? { 12 | let jsonDecoder: JSONDecoder = JSONDecoder() 13 | guard let decodedData = try? jsonDecoder.decode(T.self, from: self) else { return nil } 14 | return decodedData 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MedExp - SwiftUI MVVM 2 | This application was created to answer the question of how we can implement the MVVM design pattern in a testable way with SwiftUI. For details and explanation of its content, you can **fork** the project and follow the Medium article. 3 | 4 | ![Slide 16_9 - 1](https://github.com/devmehmetates/MedExp-MVVM/assets/74152011/c0d0449f-1e80-4b84-9576-20fd3026e23b) 5 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Utils/CardProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardProtocol.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 11.11.2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | protocol CardView { } 11 | extension CardView { 12 | func cardBackground(overlayLinearGradient: LinearGradient, colorScheme: ColorScheme) -> some View { 13 | Rectangle() 14 | .foregroundStyle(overlayLinearGradient) 15 | .cornerRadius(12) 16 | .shadow(color: .gray.opacity(0.3), radius: colorScheme == .dark ? 0 : 8) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Models/Networks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Networks.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 10.11.2022. 6 | // 7 | 8 | struct Networks: Codable, Identifiable { 9 | let id: Int 10 | private let name: String? 11 | private let logoPath: String? 12 | 13 | var networkName: String { name ?? "" } 14 | var logoImagePath: String { logoPath != nil ? NetworkManager.shared.createLogoimageUrl(withPath: logoPath) : "" } 15 | 16 | enum CodingKeys: String, CodingKey { 17 | case id, name 18 | case logoPath = "logo_path" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Utils/AppConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppConstants.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 3.11.2022. 6 | // 7 | 8 | struct AppConstants { 9 | static let shared: AppConstants = AppConstants() 10 | 11 | let exampleImagePath: String = "https://www.themoviedb.org/t/p/original/cLoSyNIijYFZu5S0ruR4wLTXoRV.jpg" 12 | let exampleImagePath2: String = "https://www.themoviedb.org/t/p/original/djM2s4wSaATn4jVB33cV05PEbV7.jpg" 13 | let exampleImagePath3: String = "https://www.themoviedb.org/t/p/original/qtuAjmUWY7xIEOg0yfpqLkHSeeu.jpg" 14 | let exampleBackdropImagePath: String = "https://www.themoviedb.org/t/p/original/rJUD2cZpQPvpzwTrlt5GlYiilTF.jpg" 15 | } 16 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "stickyasyncimageswiftui", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/devmehmetates/StickyAsyncImageSwiftUI.git", 7 | "state" : { 8 | "branch" : "main", 9 | "revision" : "6a2b91bd23c35c604dafe553d895049f16558584" 10 | } 11 | }, 12 | { 13 | "identity" : "swiftuianimatedringcharts", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/devmehmetates/SwiftUIAnimatedRingCharts.git", 16 | "state" : { 17 | "branch" : "main", 18 | "revision" : "4d315c53fbcb90657817aad04fd3a34e6e929337" 19 | } 20 | } 21 | ], 22 | "version" : 2 23 | } 24 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Components/CustomSectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomSectionView.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 11.11.2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CustomSectionView : View { 11 | private let content: Content 12 | private let title: String 13 | private let titleFont: Font 14 | 15 | init(title: String, font: Font? = nil, @ViewBuilder content: @escaping () -> Content) { 16 | self.title = title 17 | self.content = content() 18 | self.titleFont = font ?? .title 19 | } 20 | 21 | var body: some View { 22 | VStack(spacing: 5){ 23 | HStack { 24 | Text(title) 25 | .font(titleFont) 26 | Spacer() 27 | }.padding(.horizontal) 28 | content 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Utils/RequestAbleMovieListProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestableMediaListProtocol.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 7.11.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol RequestableMediaListProtocol: ObservableObject { } 11 | 12 | extension RequestableMediaListProtocol { 13 | func handleMediaListApiRequests(endPoint: URL, manager: NetworkManagerProtocol, completion: ((_ mediaList: [Media]) -> Void)? = nil) { 14 | manager.apiRequest(endpoint: endPoint, param: nil) { response in 15 | switch response { 16 | case .success(let data): 17 | guard let decodedData: MediaList = data.decodedModel() else { return } 18 | completion?(decodedData.mediaList) 19 | case .failure(_): 20 | print("err") 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Models/Actor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Actor.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 11.11.2022. 6 | // 7 | 8 | struct Actor: Codable, Identifiable { 9 | let id: Int 10 | private let name: String? 11 | private let originalName: String? 12 | private let profilePath: String? 13 | private let roles: [Role?]? 14 | private let order: Int? 15 | 16 | var actorName: String { originalName ?? name ?? "" } 17 | var profileImagePath: String { NetworkManager.shared.createPosterimageUrl(withPath: profilePath) } 18 | var actorRoles: Role? { (roles ?? []).compactMap{ $0 }.first } 19 | var actorOrder: Int { order ?? 99 } 20 | 21 | enum CodingKeys: String, CodingKey { 22 | case id, name, roles, order 23 | case originalName = "original_name" 24 | case profilePath = "profile_path" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Components/HeaderCarouselMediaList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeaderCarouselMediaList.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 9.11.2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HeaderCarouselMediaList: View { 11 | @Environment(\.colorScheme) private var colorScheme 12 | private var backgroundColor: Color { colorScheme == .dark ? .black.opacity(0.9) : .white.opacity(0.9) } 13 | let mediaList: [Media] 14 | 15 | var body: some View { 16 | TabView { 17 | ForEach(mediaList, id: \.id) { media in 18 | NavigationLink { 19 | LazyNavigate(DetailView(viewModel: DetailViewModel(mediaId: Int(media.id), mediaType: .tvShow))) 20 | } label: { 21 | AnimatedAsyncImageView(path: media.backdropImage) 22 | } 23 | } 24 | }.tabViewStyle(.page) 25 | .frame(width: 92.0.responsizeW, height: 40.0.responsizeW) 26 | .cornerRadius(10) 27 | .padding(.bottom) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Mehmet Ateş  4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Modules/Tabbar/TabbarRoot.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabbarRoot.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 6.11.2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TabbarRoot: View { 11 | @State private var currentTab: String = "Home" 12 | 13 | var body: some View { 14 | TabView(selection: $currentTab) { 15 | HomeView(viewModel: HomeViewModel()) 16 | .tag("Home") 17 | SearchView(viewModel: SearchViewModel()) 18 | .tag("Search") 19 | }.overlay(alignment: .bottom) { 20 | bottomTabbarStack 21 | }.ignoresSafeArea(.keyboard, edges: .bottom) 22 | } 23 | } 24 | 25 | // MARK: View Component(s) 26 | extension TabbarRoot { 27 | private var bottomTabbarStack: some View { 28 | HStack { 29 | TabbarButton(currentTab: $currentTab, title: "Home", icon: "rectangle.portrait") 30 | TabbarButton(currentTab: $currentTab, title: "Search", icon: "magnifyingglass") 31 | } 32 | } 33 | } 34 | 35 | struct TabbarRoot_Previews: PreviewProvider { 36 | static var previews: some View { 37 | TabbarRoot() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Components/LogoImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogoImageView.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 11.11.2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LogoImageView: View { 11 | @Environment(\.colorScheme) var colorScheme 12 | private var backgroundColor: Color { colorScheme == .dark ? .white : .gray.opacity(0.1) } 13 | let imagePath: String 14 | 15 | var body: some View { 16 | HStack { 17 | if !imagePath.isEmpty { 18 | AnimatedAsyncImageView(path: imagePath, cornerRadius: 0, scaleType: .toFit) 19 | .frame(width: 10.0.responsizeW, height: 10.0.responsizeW) 20 | } else { 21 | VStack { 22 | Image(systemName: "globe") 23 | .resizable() 24 | .foregroundColor(.primary) 25 | .frame(width: 5.0.responsizeW, height: 5.0.responsizeW) 26 | }.frame(width: 10.0.responsizeW, height: 10.0.responsizeW) 27 | } 28 | }.padding(1.0.responsizeW) 29 | .background(backgroundColor) 30 | .cornerRadius(5) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-ExampleTests/MedExp_DetailViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MedExp_DetailViewModelTests.swift 3 | // SwiftUI-MVVM-ExampleTests 4 | // 5 | // Created by Mehmet Ateş on 20.11.2022. 6 | // 7 | 8 | import XCTest 9 | @testable import SwiftUI_MVVM_Example 10 | 11 | final class MedExp_DetailViewModelTests: XCTestCase { 12 | private var detailViewModel: DetailViewModel! 13 | private var networkManager: NetworkManagerMock! 14 | 15 | override func setUpWithError() throws { 16 | try super.setUpWithError() 17 | networkManager = NetworkManagerMock() 18 | detailViewModel = DetailViewModel(mediaId: 95403, mediaType: .tvShow, manager: networkManager) 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | try super.tearDownWithError() 23 | detailViewModel = nil 24 | networkManager = nil 25 | } 26 | 27 | func testHandleMedias() { 28 | XCTAssertTrue(networkManager.invokedCreateRequestURL) 29 | XCTAssertEqual(networkManager.invokedCreateRequestURLCount, 4) 30 | XCTAssertTrue(networkManager.invokedApiRequest) 31 | XCTAssertEqual(networkManager.invokedApiRequestCount, 4) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Modules/Tabbar/TabbarButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabbarButton.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 6.11.2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TabbarButton: View { 11 | @Binding var currentTab: String 12 | private var isSelected: Bool { currentTab == title } 13 | let title: String 14 | let icon: String 15 | 16 | var body: some View { 17 | Button(action: buttonAction) { 18 | buttonLabel 19 | } 20 | } 21 | } 22 | 23 | // MARK: - View Component(s) 24 | extension TabbarButton { 25 | private var buttonLabel: some View { 26 | VStack { 27 | Image(systemName: icon) 28 | Text(title) 29 | }.frame(maxWidth: .infinity) 30 | .foregroundColor(isSelected ? .accentColor : .secondary) 31 | } 32 | 33 | private func buttonAction() { 34 | withAnimation { 35 | currentTab = title 36 | } 37 | } 38 | } 39 | 40 | struct TabbarButton_Previews: PreviewProvider { 41 | static var previews: some View { 42 | HStack { 43 | TabbarButton(currentTab: .constant("Home"), title: "Home", icon: "rectangle.portrait") 44 | TabbarButton(currentTab: .constant("Home"), title: "Search", icon: "magnifyingglass") 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Components/YoutubeVideoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YoutubeVideoView.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 11.11.2022. 6 | // 7 | 8 | import SwiftUI 9 | import WebKit 10 | 11 | struct YoutubeVideoView: View { 12 | private let key: String 13 | 14 | init(_ videoKey: String) { 15 | self.key = videoKey 16 | } 17 | 18 | var body: some View { 19 | YoutubeVideoViewBranch(key) 20 | .frame(width: 80.0.responsizeW, height: 50.0.responsizeW) 21 | .background(.ultraThickMaterial) 22 | .cornerRadius(5) 23 | } 24 | } 25 | 26 | struct YoutubeVideoViewBranch: UIViewRepresentable { 27 | private let key: String 28 | 29 | init(_ videoKey: String) { 30 | self.key = videoKey 31 | } 32 | 33 | func makeUIView(context: Context) -> WKWebView { 34 | WKWebView() 35 | } 36 | 37 | func updateUIView(_ uiView: WKWebView, context: Context) { 38 | let path = "https://www.youtube.com/embed/\(key)" 39 | guard let url = URL(string: path) else { return } 40 | uiView.scrollView.isScrollEnabled = false 41 | uiView.load(.init(url: url)) 42 | } 43 | } 44 | 45 | struct YoutubeVideoCiew_Previews: PreviewProvider { 46 | static var previews: some View { 47 | YoutubeVideoView("pycigmeHVmM") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-ExampleTests/MedExp_HomeViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MedExp_HomeViewModelTests.swift 3 | // SwiftUI-MVVM-ExampleTests 4 | // 5 | // Created by Mehmet Ateş on 19.11.2022. 6 | // 7 | 8 | import XCTest 9 | @testable import SwiftUI_MVVM_Example 10 | 11 | final class MedExp_HomeViewModelTests: XCTestCase { 12 | private var homeViewModel: HomeViewModel! 13 | private var networkManager: NetworkManagerMock! 14 | 15 | override func setUpWithError() throws { 16 | try super.setUpWithError() 17 | networkManager = NetworkManagerMock() 18 | homeViewModel = HomeViewModel(manager: networkManager) 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | try super.tearDownWithError() 23 | homeViewModel = nil 24 | networkManager = nil 25 | } 26 | 27 | func testSectionInit() { 28 | XCTAssertTrue(networkManager.invokedApiRequest) 29 | XCTAssertEqual(networkManager.invokedApiRequestCount, 6) 30 | } 31 | 32 | func testCreateSectionEndpoint() { 33 | XCTAssertTrue(networkManager.invokedCreateRequestURL) 34 | XCTAssertEqual(networkManager.invokedCreateRequestURLCount, 6) 35 | } 36 | 37 | func testIncreasePage() { 38 | homeViewModel.increasePage(withSectionKey: "Popular on TV") 39 | XCTAssertEqual(networkManager.invokedApiRequestCount, 7) 40 | XCTAssertEqual(networkManager.invokedCreateRequestURLCount, 7) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Modules/Search/SearchView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchView.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 6.11.2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SearchView: View where Model: SearchViewModelProtocol { 11 | private let columns: [GridItem] = [.init(.fixed(45.0.responsizeW)), .init(.fixed(45.0.responsizeW))] 12 | @ObservedObject var viewModel: Model 13 | 14 | var body: some View { 15 | NavigationView { 16 | ScrollView { 17 | LazyVGrid(columns: columns) { 18 | ForEach(Array(zip(viewModel.searchList.indices, viewModel.searchList)), id: \.0) { index, media in 19 | SearchMediaCardView(media: media) 20 | .onAppear { 21 | viewModel.setPage(index) 22 | } 23 | } 24 | } 25 | }.navigationTitle("Search") 26 | .searchable(text: $viewModel.searchKey) 27 | .disableAutocorrection(true) 28 | .keyboardType(.namePhonePad) 29 | } 30 | } 31 | } 32 | 33 | struct SearchView_Previews: PreviewProvider { 34 | static var previews: some View { 35 | let viewModel = SearchViewModel() 36 | SearchView(viewModel: viewModel) 37 | .onAppear { 38 | viewModel.searchKey = "Titanic" 39 | } 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Components/AnimatedAsyncImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimatedAsyncImageView.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 3.11.2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AnimatedAsyncImageView: View { 11 | private var url: URL? { 12 | URL(string: path) 13 | } 14 | 15 | let path: String 16 | var cornerRadius: Double? 17 | var scaleType: ScaleTypes? = .toFill 18 | 19 | var body: some View { 20 | GeometryReader { proxy in 21 | AsyncImage(url: url, transaction: .init(animation: .easeInOut)) { phase in 22 | if let image = phase.image { 23 | if scaleType! == .toFill { 24 | image.resizable() 25 | .scaledToFill() 26 | .clipped() 27 | } else { 28 | image.resizable() 29 | .scaledToFit() 30 | .clipped() 31 | } 32 | } else { 33 | Rectangle() 34 | .foregroundColor(.clear) 35 | .background(.ultraThickMaterial) 36 | } 37 | }.frame(width: proxy.size.width, height: proxy.size.height, alignment: .center) 38 | .cornerRadius(cornerRadius ?? 10.0) 39 | } 40 | } 41 | } 42 | 43 | struct AnimatedAsyncImageView_Previews: PreviewProvider { 44 | static var previews: some View { 45 | AnimatedAsyncImageView(path: AppConstants.shared.exampleImagePath) 46 | } 47 | } 48 | 49 | enum ScaleTypes { 50 | case toFit 51 | case toFill 52 | } 53 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Components/ActorCardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActorCardView.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 11.11.2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ActorCardView: View, CardView { 11 | @Environment(\.colorScheme) private var colorScheme 12 | var actor: Actor 13 | 14 | var body: some View { 15 | VStack { 16 | VStack { 17 | AnimatedAsyncImageView(path: actor.profileImagePath) 18 | HStack { 19 | VStack(alignment: .leading) { 20 | Text(actor.actorName) 21 | .font(.system(size: 4.0.responsizeW, weight: .semibold)) 22 | if let roleName = actor.actorRoles?.roleName { 23 | Text("(\(roleName))") 24 | .font(.caption2) 25 | } 26 | } 27 | Spacer() 28 | }.frame(height: 10.0.responsizeW) 29 | .padding(.horizontal, 7) 30 | }.padding(.bottom, 7) 31 | }.frame(width: 40.0.responsizeW, height: 65.0.responsizeW) 32 | .background(cardBackground(overlayLinearGradient: overlayLinearGradient, colorScheme: colorScheme)) 33 | } 34 | } 35 | 36 | // MARK: Color properties 37 | extension ActorCardView { 38 | private var overlayLinearGradient: LinearGradient { 39 | LinearGradient( 40 | colors: [.clear, backgroundColor.opacity(0.2), backgroundColor.opacity(0.5), backgroundColor.opacity(0.8)], 41 | startPoint: .top, 42 | endPoint: .bottom 43 | ) 44 | } 45 | private var backgroundColor: Color { colorScheme == .dark ? .gray.opacity(0.2) : .white } 46 | } 47 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Modules/Home/HomeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeView.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 6.11.2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HomeView: View where Model: HomeViewModelProtocol { 11 | @ObservedObject var viewModel: Model 12 | 13 | var body: some View { 14 | NavigationView { 15 | Group { 16 | if viewModel.isPageLoaded { 17 | loadedState 18 | } else { 19 | loadingState 20 | } 21 | }.navigationTitle("What's Popular") 22 | }.tag("Home") 23 | } 24 | } 25 | 26 | // MARK: - View Component(s) 27 | extension HomeView { 28 | private var loadedState: some View { 29 | ScrollView { 30 | if let headerMediaList: [Media] = viewModel.mediaSections["Popular on TV"]?.mediaList { 31 | HeaderCarouselMediaList(mediaList: headerMediaList) 32 | } 33 | movieSectionStacks 34 | } 35 | } 36 | 37 | private var movieSectionStacks: some View { 38 | LazyVStack { 39 | ForEach(Array(viewModel.mediaSections.keys).sorted(), id: \.self) { sectionKey in 40 | if let mediaSectionValue: MediaSectionValue = viewModel.mediaSections[sectionKey] { 41 | MediaListSection(mediaList: mediaSectionValue.mediaList, sectionTitle: sectionKey, mediaType: mediaSectionValue.type) { 42 | viewModel.increasePage(withSectionKey: sectionKey) 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | 50 | struct HomeView_Previews: PreviewProvider { 51 | static var previews: some View { 52 | HomeView(viewModel: HomeViewModel()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Modules/Search/SearchViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchViewModel.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 7.11.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol SearchViewModelProtocol: RequestableMediaListProtocol { 11 | var searchList: [Media] { get } 12 | var searchKey: String { get set } 13 | func setPage(_ index: Int) 14 | } 15 | 16 | class SearchViewModel: SearchViewModelProtocol { 17 | @Published var searchList: [Media] = [] 18 | @Published var page: Int = 1 { 19 | didSet { 20 | handleSearchMedia() 21 | } 22 | } 23 | @Published var searchKey: String = "" { 24 | didSet { 25 | searchKeyDidSet() 26 | } 27 | } 28 | 29 | private var manager: NetworkManagerProtocol! 30 | 31 | init(manager: NetworkManagerProtocol? = nil) { 32 | self.manager = manager ?? NetworkManager() 33 | } 34 | } 35 | 36 | // MARK: - Interface Setup 37 | extension SearchViewModel { 38 | func setPage(_ index: Int) { 39 | if index == searchList.count - 3 { 40 | page < 10 ? page += 1 : nil 41 | } 42 | 43 | #if DEBUG 44 | if index == -1 { 45 | page += 1 46 | } 47 | #endif 48 | } 49 | } 50 | 51 | // MARK: - Logic(s) 52 | extension SearchViewModel { 53 | private func searchKeyDidSet() { 54 | searchList.removeAll() 55 | if searchKey.isEmpty { 56 | page = 1 57 | } 58 | handleSearchMedia() 59 | } 60 | } 61 | 62 | // MARK: - Api process 63 | extension SearchViewModel { 64 | private func handleSearchMedia() { 65 | let url = manager.createRequestURL(ApiEndpoints.search.rawValue, pathVariables: nil, headerParams: [ 66 | "page": page, 67 | "query": searchKey, 68 | ]) 69 | handleMediaListApiRequests(endPoint: url, manager: manager) { [weak self] mediaList in 70 | DispatchQueue.main.async { 71 | self?.searchList += mediaList.filter { $0.title != "Unknowed" } 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Components/MediaListSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaListSection.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 6.11.2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MediaListSection: View { 11 | let mediaList: [Media] 12 | let sectionTitle: String 13 | let titleFont: Font 14 | let mediaType: MediaTypes 15 | var cardOnAppear: (() -> Void)? = nil 16 | 17 | init(mediaList: [Media], sectionTitle: String, titleFont: Font? = nil, mediaType: MediaTypes, cardOnAppear: ( () -> Void)? = nil) { 18 | self.mediaList = mediaList 19 | self.sectionTitle = sectionTitle 20 | self.titleFont = titleFont ?? .title2 21 | self.mediaType = mediaType 22 | self.cardOnAppear = cardOnAppear 23 | } 24 | 25 | var body: some View { 26 | CustomSectionView(title: sectionTitle, font: titleFont) { 27 | mediaHorizontalScrollList 28 | } 29 | } 30 | } 31 | 32 | // MARK: - View Component(s) 33 | extension MediaListSection { 34 | private var mediaHorizontalScrollList: some View { 35 | ScrollView(.horizontal, showsIndicators: false) { 36 | LazyHStack { 37 | ForEach(Array(zip(mediaList.indices, mediaList)), id: \.1.id) { index, media in 38 | NavigationLink { 39 | LazyNavigate(DetailView(viewModel: DetailViewModel(mediaId: Int(media.id), mediaType: mediaType))) 40 | } label: { 41 | MediaCardView(media: media) 42 | .onAppear { 43 | if index == mediaList.count - 3 { 44 | cardOnAppear?() 45 | } 46 | } 47 | } 48 | } 49 | }.padding([.horizontal, .bottom]) 50 | } 51 | } 52 | } 53 | 54 | struct MediaListSection_Previews: PreviewProvider { 55 | static var previews: some View { 56 | MediaListSection(mediaList: [], sectionTitle: "On Tv", mediaType: .movie) { 57 | 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-ExampleTests/Med_Exp_SearchViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Med_Exp_SearchViewModelTests.swift 3 | // SwiftUI-MVVM-ExampleTests 4 | // 5 | // Created by Mehmet Ateş on 20.11.2022. 6 | // 7 | 8 | import XCTest 9 | @testable import SwiftUI_MVVM_Example 10 | 11 | final class Med_Exp_SearchViewModelTests: XCTestCase { 12 | private var searchViewModel: SearchViewModel! 13 | private var networkManager: NetworkManagerMock! 14 | 15 | override func setUpWithError() throws { 16 | try super.setUpWithError() 17 | networkManager = NetworkManagerMock() 18 | searchViewModel = SearchViewModel(manager: networkManager) 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | try super.tearDownWithError() 23 | searchViewModel = nil 24 | networkManager = nil 25 | } 26 | 27 | func testSetPage() { 28 | searchViewModel.setPage(-1) 29 | XCTAssertTrue(networkManager.invokedCreateRequestURL) 30 | XCTAssertEqual(networkManager.invokedCreateRequestURLCount, 1) 31 | XCTAssertTrue(networkManager.invokedApiRequest) 32 | XCTAssertEqual(networkManager.invokedApiRequestCount, 1) 33 | } 34 | 35 | func testSetSearchKey_KeySetted() { 36 | searchViewModel.searchKey = "Titanic" 37 | XCTAssertTrue(searchViewModel.searchList.isEmpty) 38 | XCTAssertTrue(networkManager.invokedCreateRequestURL) 39 | XCTAssertEqual(networkManager.invokedCreateRequestURLCount, 1) 40 | XCTAssertTrue(networkManager.invokedApiRequest) 41 | XCTAssertEqual(networkManager.invokedApiRequestCount, 1) 42 | } 43 | 44 | func testSetSearchKey_KeyEmpty() { 45 | searchViewModel.setPage(-1) // First 46 | searchViewModel.searchKey = "" // Second - Third 47 | XCTAssertTrue(searchViewModel.searchList.isEmpty) 48 | XCTAssertEqual(searchViewModel.page, 1) 49 | XCTAssertTrue(networkManager.invokedCreateRequestURL) 50 | XCTAssertEqual(networkManager.invokedCreateRequestURLCount, 3) 51 | XCTAssertTrue(networkManager.invokedApiRequest) 52 | XCTAssertEqual(networkManager.invokedApiRequestCount, 3) 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Components/MediaCardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaCardView.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 2.11.2022. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftUIAnimatedRingCharts 10 | 11 | struct MediaCardView: View, CardView { 12 | @Environment(\.colorScheme) private var colorScheme 13 | let media: Media 14 | 15 | var body: some View { 16 | VStack { 17 | VStack { 18 | AnimatedAsyncImageView(path: media.posterImage) 19 | mediaInformationStack 20 | }.padding(.bottom, 7) 21 | }.frame(width: 45.0.responsizeW, height: 85.0.responsizeW) 22 | .background(cardBackground(overlayLinearGradient: overlayLinearGradient, colorScheme: colorScheme)) 23 | } 24 | } 25 | 26 | // MARK: View component(s) 27 | extension MediaCardView { 28 | private var mediaTitle: some View { 29 | Text(media.title) 30 | .lineLimit(3) 31 | .multilineTextAlignment(.leading) 32 | .font(.caption2) 33 | .foregroundColor(.primary) 34 | } 35 | 36 | private var mediaPointChartStack: some View { 37 | ZStack { 38 | RingChartsView(values: [media.point], colors: [[contentPointColor, contentPointColorSecond]], ringsMaxValue: 100, lineWidth: 1.2.responsizeW) 39 | .frame(width: 11.0.responsizeW, height: 11.0.responsizeW) 40 | Text("%\(Int(media.point).formatted())") 41 | .font(.caption2) 42 | .foregroundColor(.primary) 43 | } 44 | } 45 | 46 | private var mediaInformationStack: some View { 47 | HStack { 48 | mediaTitle 49 | Spacer() 50 | mediaPointChartStack 51 | }.padding(.horizontal, 11) 52 | .padding(.vertical, 7) 53 | .font(.callout) 54 | } 55 | } 56 | 57 | // MARK: Color properties 58 | extension MediaCardView { 59 | private var overlayLinearGradient: LinearGradient { 60 | LinearGradient( 61 | colors: [.clear, backgroundColor.opacity(0.2), backgroundColor.opacity(0.5), backgroundColor.opacity(0.8)], 62 | startPoint: .top, 63 | endPoint: .bottom 64 | ) 65 | } 66 | private var backgroundColor: Color { colorScheme == .dark ? .gray.opacity(0.2) : .white } 67 | private var contentPointColor: Color { media.point < 50 ? .red : media.point < 80 ? .yellow : .green } 68 | private var contentPointColorSecond: Color { contentPointColor.opacity(0.4) } 69 | } 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | SwiftUI-MVVM-Example/Core/AppEnvironments.swift 92 | .DS_Store 93 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Components/SearchMovieCardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchMediaCardView.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 9.11.2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SearchMediaCardView: View { 11 | @Environment(\.colorScheme) var colorScheme 12 | let media: Media 13 | @State private var isInformationOn: Bool = false 14 | @State private var isActive = false 15 | 16 | var body: some View { 17 | ZStack(alignment: .bottom) { 18 | cardBody 19 | .offset(y: isInformationOn ? -55.0.responsizeW : 0) 20 | cardInformation 21 | .offset(y: isInformationOn ? 0 : 50.0.responsizeW) 22 | .opacity(isInformationOn ? 1 : 0) 23 | }.onTapGesture { 24 | isActive.toggle() 25 | }.onLongPressGesture { 26 | withAnimation(.spring()) { 27 | isInformationOn.toggle() 28 | } 29 | }.onDisappear{ 30 | isInformationOn = false 31 | }.clipped() 32 | .background( 33 | NavigationLink(destination: LazyNavigate(DetailView(viewModel: DetailViewModel(mediaId: Int(media.id), mediaType: media.type ?? .tvShow))),isActive: $isActive) { 34 | EmptyView() 35 | }) 36 | } 37 | } 38 | 39 | // MARK: View Component(s) 40 | extension SearchMediaCardView { 41 | private var mediaInformationStack: some View { 42 | VStack(alignment: .leading) { 43 | Text(media.title) 44 | .font(.title3) 45 | .fontWeight(.bold) 46 | .lineLimit(2) 47 | Text("(\(media.releaseYear))") 48 | Spacer() 49 | Text(media.overview) 50 | .font(.caption) 51 | .lineLimit(6) 52 | Spacer() 53 | }.padding() 54 | .frame(width: 45.0.responsizeW, height: 50.0.responsizeW) 55 | .background(.quaternary) 56 | } 57 | 58 | private var cardBody: some View { 59 | ZStack(alignment: .bottomTrailing) { 60 | AnimatedAsyncImageView(path: media.posterImage) 61 | .aspectRatio(0.65, contentMode: .fill) 62 | LinearGradient(colors: [.clear, .clear, .black.opacity(0.8)], startPoint: .top, endPoint: .bottom) 63 | HStack(spacing: 5) { 64 | Text("Press Long") 65 | Image(systemName: "hand.tap.fill") 66 | }.font(.caption2) 67 | .padding() 68 | .foregroundColor(.white) 69 | }.cornerRadius(10) 70 | } 71 | 72 | private var cardInformation: some View { 73 | mediaInformationStack 74 | .cornerRadius(10) 75 | } 76 | } 77 | 78 | // MARK: Color properties 79 | extension SearchMediaCardView { 80 | private var backgroundColor: Color { colorScheme == .dark ? .black : .white } 81 | } 82 | 83 | struct SearchMediaCard_Previews: PreviewProvider { 84 | static var previews: some View { 85 | let viewModel = SearchViewModel() 86 | SearchView(viewModel: viewModel) 87 | .onAppear { 88 | viewModel.searchKey = "Titanic" 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Models/Media.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Media.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 6.11.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Media: Codable, Identifiable { 11 | let id: Double 12 | private let adult: Bool? 13 | private let backdropPath: String? 14 | private let firstAirDate: String? 15 | private let genres: [Genres?]? 16 | private let homepage: String? 17 | private let name: String? 18 | private let networks: [Networks?]? 19 | private let productionCompanies: [Networks?]? 20 | private let numberOfEpisodes: Int? 21 | private let numberOfSeasons: Int? 22 | private let originalName: String? 23 | private let originalTitle: String? 24 | private let overView: String? 25 | private let posterPath: String? 26 | private let voteAverage: Double? 27 | private let releaseDate: String? 28 | private let mediaType: String? 29 | 30 | init() { 31 | self.id = 0 32 | self.adult = nil 33 | self.backdropPath = nil 34 | self.firstAirDate = nil 35 | self.genres = nil 36 | self.homepage = nil 37 | self.name = nil 38 | self.networks = nil 39 | self.productionCompanies = nil 40 | self.numberOfEpisodes = nil 41 | self.numberOfSeasons = nil 42 | self.originalName = nil 43 | self.originalTitle = nil 44 | self.overView = nil 45 | self.posterPath = nil 46 | self.voteAverage = nil 47 | self.releaseDate = nil 48 | self.mediaType = nil 49 | } 50 | 51 | var isAdult: Bool { adult ?? false } 52 | var backdropImage: String { NetworkManager.shared.createBackdropimageUrl(withPath: backdropPath) } 53 | var originalBackdropImage: String { NetworkManager.shared.createOriginalImageUrl(withPath: backdropPath) } 54 | var mediaGenres: [Genres] { (genres ?? []).compactMap { $0 } } 55 | var mediaHomePage: String { homepage ?? "" } 56 | var title: String { name ?? originalName ?? originalTitle ?? "Unknowed" } 57 | var mediaNetworks: [Networks] { (networks ?? productionCompanies ?? []).compactMap { $0 } } 58 | var episodesCount: Int { numberOfEpisodes ?? 0 } 59 | var sessionCount: Int { numberOfSeasons ?? 0 } 60 | var overview: String { overView ?? "Unknowed" } 61 | var posterImage: String { NetworkManager.shared.createPosterimageUrl(withPath: posterPath) } 62 | var point: CGFloat { (voteAverage ?? 0) * 10 } 63 | var releaseYear: String { releaseDate?.split(separator: "-").first?.description ?? firstAirDate?.split(separator: "-").first?.description ?? "Unknowed" } 64 | var type: MediaTypes? { 65 | guard let mediaType else { return nil } 66 | return mediaType == MediaTypes.movie.rawValue ? .movie : .tvShow 67 | } 68 | 69 | enum CodingKeys: String, CodingKey { 70 | case id, adult, genres, homepage, name, networks 71 | case backdropPath = "backdrop_path" 72 | case firstAirDate = "first_air_date" 73 | case productionCompanies = "production_companies" 74 | case numberOfEpisodes = "number_of_episodes" 75 | case numberOfSeasons = "number_of_seasons" 76 | case originalName = "original_name" 77 | case originalTitle = "original_title" 78 | case overView = "overview" 79 | case posterPath = "poster_path" 80 | case voteAverage = "vote_average" 81 | case releaseDate = "release_date" 82 | case mediaType = "media_type" 83 | } 84 | } 85 | 86 | enum MediaTypes: String { 87 | case movie = "movie" 88 | case tvShow = "tv" 89 | } 90 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example.xcodeproj/xcshareddata/xcschemes/SwiftUI-MVVM-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 34 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 71 | 73 | 79 | 80 | 81 | 82 | 84 | 85 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Modules/Home/HomeViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeViewModel.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 6.11.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol HomeViewModelProtocol: RequestableMediaListProtocol { 11 | var mediaSections: [String: MediaSectionValue] { get } 12 | var isPageLoaded: Bool { get } 13 | 14 | func increasePage(withSectionKey key: String) 15 | } 16 | 17 | class HomeViewModel: HomeViewModelProtocol { 18 | @Published var mediaSections: [String: MediaSectionValue] = [:] 19 | @Published var isPageLoaded: Bool = false 20 | private var manager: NetworkManagerProtocol! 21 | 22 | init(manager: NetworkManagerProtocol? = nil) { 23 | self.manager = manager ?? NetworkManager() 24 | sectionInit() 25 | } 26 | } 27 | 28 | // MARK: - Api process 29 | extension HomeViewModel { 30 | private func sectionInit() { 31 | for (mediaSectionKey, mediaSectionValues) in mediaSectionInitilizer { 32 | let endpoint: URL = createSectionEndpoint(sectionKey: mediaSectionValues.endpoint, page: 1) 33 | handleMediaListApiRequests(endPoint: endpoint, manager: manager) { [weak self] mediaList in 34 | DispatchQueue.main.async { 35 | self?.mediaSections[mediaSectionKey] = MediaSectionValue(mediaList: mediaList, page: 1, type: mediaSectionValues.type) 36 | self?.filtMediaListQuality(mediaSectionKey) 37 | self?.isPageLoaded = true 38 | } 39 | } 40 | } 41 | } 42 | 43 | private func createSectionEndpoint(sectionKey key: String, page: Int, pathVariables: [String]? = nil) -> URL { 44 | manager.createRequestURL(key, pathVariables: pathVariables, headerParams: [ 45 | "page": page, 46 | ]) 47 | } 48 | 49 | private func updateSection(_ key: String, endpointRawValue: String, page: Int) { 50 | let endpoint: URL = createSectionEndpoint(sectionKey: endpointRawValue, page: page) 51 | handleMediaListApiRequests(endPoint: endpoint, manager: manager) { [weak self] mediaList in 52 | DispatchQueue.main.async { 53 | self?.mediaSections[key]?.mediaList += mediaList 54 | self?.filtMediaListQuality(key) 55 | } 56 | } 57 | } 58 | } 59 | 60 | // MARK: - Request Properties 61 | extension HomeViewModel { 62 | private var mediaSectionInitilizer: [String: MediaSectionInitilizerValue] { 63 | [ 64 | "Popular on TV": MediaSectionInitilizerValue(endpoint: ApiEndpoints.popularOnTV.rawValue, type: .tvShow), 65 | "Top Rated on TV": MediaSectionInitilizerValue(endpoint: ApiEndpoints.topRatedTV.rawValue, type: .tvShow), 66 | "Popular Movies": MediaSectionInitilizerValue(endpoint: ApiEndpoints.popularMovies.rawValue, type: .movie), 67 | "Top Rated Movies": MediaSectionInitilizerValue(endpoint: ApiEndpoints.topRatedMovies.rawValue, type: .movie), 68 | "Trending Movies": MediaSectionInitilizerValue(endpoint: ApiEndpoints.trendingMovie.rawValue, type: .movie), 69 | "Trending TV": MediaSectionInitilizerValue(endpoint: ApiEndpoints.trendingTv.rawValue, type: .tvShow) 70 | ] 71 | } 72 | } 73 | 74 | // MARK: - Interface Properties 75 | extension HomeViewModel { 76 | func increasePage(withSectionKey key: String) { 77 | mediaSections[key]?.page += 1 78 | let page = mediaSections[key]?.page ?? 0 79 | guard page < 10 else { return } 80 | let endpointRawValue = mediaSectionInitilizer[key]?.endpoint ?? "" 81 | updateSection(key, endpointRawValue: endpointRawValue, page: page) 82 | } 83 | } 84 | 85 | // MARK: - Filter Logic 86 | extension HomeViewModel { 87 | private func filtMediaListQuality(_ key: String) { 88 | mediaSections[key]?.mediaList = mediaSections[key]?.mediaList.filter { !$0.overview.isEmpty } ?? [] 89 | } 90 | } 91 | 92 | struct MediaSectionValue { 93 | var mediaList: [Media] 94 | var page: Int 95 | var type: MediaTypes 96 | } 97 | 98 | struct MediaSectionInitilizerValue { 99 | var endpoint: String 100 | var type: MediaTypes 101 | } 102 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Network/NetworkManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkManager.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 4.11.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol NetworkManagerProtocol { 11 | static var shared: NetworkManager { get } 12 | func apiRequest(endpoint: URL, param: Data?, completion: @escaping (Result) -> Void) 13 | func createPosterimageUrl(withPath path: String?) -> String 14 | func createBackdropimageUrl(withPath path: String?) -> String 15 | func createOriginalImageUrl(withPath path: String?) -> String 16 | func createLogoimageUrl(withPath path: String?) -> String 17 | func createRequestURL(_ endpoint: String, pathVariables: [String]?, headerParams: [String: Any]?) -> URL 18 | } 19 | 20 | struct NetworkManager: NetworkManagerProtocol { 21 | static let shared: NetworkManager = NetworkManager() 22 | 23 | func apiRequest(endpoint: URL, param: Data? = nil, completion: @escaping (Result) -> Void) { 24 | var request = URLRequest(url: endpoint) 25 | let method = param != nil ? HttpMethods.post.rawValue : HttpMethods.get.rawValue 26 | request.httpMethod = method 27 | request.httpBody = param 28 | 29 | print("URL LOG:", endpoint.description) 30 | 31 | URLSession.shared.dataTask(with: request) { data, response, err in 32 | guard let data else { return completion(.failure(.emptyData)) } 33 | return completion(.success(data)) 34 | }.resume() 35 | } 36 | } 37 | 38 | // MARK: - Image function(s) 39 | extension NetworkManager { 40 | func createPosterimageUrl(withPath path: String?) -> String { 41 | "https://www.themoviedb.org/t/p/w342\(path ?? "")" 42 | } 43 | 44 | func createBackdropimageUrl(withPath path: String?) -> String { 45 | "https://www.themoviedb.org/t/p/w780\(path ?? "")" 46 | } 47 | 48 | func createOriginalImageUrl(withPath path: String?) -> String { 49 | "https://www.themoviedb.org/t/p/original\(path ?? "")" 50 | } 51 | 52 | func createLogoimageUrl(withPath path: String?) -> String { 53 | "https://www.themoviedb.org/t/p/w92\(path ?? "")" 54 | } 55 | } 56 | 57 | // MARK: - Request URL Function(s) 58 | extension NetworkManager { 59 | func createRequestURL(_ endpoint: String, pathVariables: [String]? = nil, headerParams: [String: Any]? = nil) -> URL { 60 | var requestParams: String = "" 61 | requestParams += ("?api_key=\(AppEnvironments.apiKey)") 62 | for (key, value) in (headerParams ?? [:]) { 63 | if key == "query" { 64 | requestParams += ("&\(key)=\(safeQuery(query: value as? String ?? ""))") 65 | } else { 66 | requestParams += ("&\(key)=\(value)") 67 | } 68 | } 69 | 70 | var pathVariable: String = "" 71 | for variable in (pathVariables ?? []) { 72 | pathVariable += "/\(variable)" 73 | } 74 | 75 | guard let url = URL(string: "https://api.themoviedb.org/3\(endpoint)\(pathVariable)\(requestParams)") else { return URL(string: AppConstants.shared.exampleImagePath)! } 76 | return url 77 | } 78 | 79 | private func safeQuery(query: String) -> String { 80 | query.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "" 81 | } 82 | } 83 | 84 | // MARK: - Endpoint(s) 85 | enum ApiEndpoints: String { 86 | // Discover Tab 87 | case popularOnTV = "/tv/popular" 88 | case popularMovies = "/movie/popular" 89 | case topRatedTV = "/tv/top_rated" 90 | case topRatedMovies = "/movie/top_rated" 91 | // Trending Tab 92 | case trendingMovie = "/trending/movie/week" 93 | case trendingTv = "/trending/tv/week" 94 | // Search Tab 95 | case search = "/search/multi" 96 | // DetailPage 97 | case tvShowDetail = "/tv" 98 | case movieShowDetail = "/movie" 99 | } 100 | 101 | // MARK: - Error(s) 102 | enum RequestErrors: Error { 103 | case wrongUrl 104 | case emptyData 105 | } 106 | 107 | // MARK: - HttpMethod(s) 108 | enum HttpMethods: String { 109 | case get = "GET" 110 | case post = "POST" 111 | } 112 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Network/Mocks/NetworkManagerMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkManagerMock.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 19.11.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | class NetworkManagerMock: NetworkManagerProtocol { 11 | static var shared: NetworkManager = NetworkManager() 12 | 13 | var invokedApiRequest = false 14 | var invokedApiRequestCount = 0 15 | var invokedApiRequestParameters: (endpoint: URL, param: Data?)? 16 | var invokedApiRequestParametersList = [(endpoint: URL, param: Data?)]() 17 | var stubbedApiRequestCompletionResult: (Result, Void)? 18 | 19 | func apiRequest(endpoint: URL, param: Data?, completion: @escaping (Result) -> Void) { 20 | invokedApiRequest = true 21 | invokedApiRequestCount += 1 22 | invokedApiRequestParameters = (endpoint, param) 23 | invokedApiRequestParametersList.append((endpoint, param)) 24 | if let result = stubbedApiRequestCompletionResult { 25 | completion(result.0) 26 | } 27 | } 28 | 29 | var invokedCreatePosterimageUrl = false 30 | var invokedCreatePosterimageUrlCount = 0 31 | var invokedCreatePosterimageUrlParameters: (path: String?, Void)? 32 | var invokedCreatePosterimageUrlParametersList = [(path: String?, Void)]() 33 | var stubbedCreatePosterimageUrlResult: String! = "" 34 | 35 | func createPosterimageUrl(withPath path: String?) -> String { 36 | invokedCreatePosterimageUrl = true 37 | invokedCreatePosterimageUrlCount += 1 38 | invokedCreatePosterimageUrlParameters = (path, ()) 39 | invokedCreatePosterimageUrlParametersList.append((path, ())) 40 | return stubbedCreatePosterimageUrlResult 41 | } 42 | 43 | var invokedCreateBackdropimageUrl = false 44 | var invokedCreateBackdropimageUrlCount = 0 45 | var invokedCreateBackdropimageUrlParameters: (path: String?, Void)? 46 | var invokedCreateBackdropimageUrlParametersList = [(path: String?, Void)]() 47 | var stubbedCreateBackdropimageUrlResult: String! = "" 48 | 49 | func createBackdropimageUrl(withPath path: String?) -> String { 50 | invokedCreateBackdropimageUrl = true 51 | invokedCreateBackdropimageUrlCount += 1 52 | invokedCreateBackdropimageUrlParameters = (path, ()) 53 | invokedCreateBackdropimageUrlParametersList.append((path, ())) 54 | return stubbedCreateBackdropimageUrlResult 55 | } 56 | 57 | var invokedCreateOriginalImageUrl = false 58 | var invokedCreateOriginalImageUrlCount = 0 59 | var invokedCreateOriginalImageUrlParameters: (path: String?, Void)? 60 | var invokedCreateOriginalImageUrlParametersList = [(path: String?, Void)]() 61 | var stubbedCreateOriginalImageUrlResult: String! = "" 62 | 63 | func createOriginalImageUrl(withPath path: String?) -> String { 64 | invokedCreateOriginalImageUrl = true 65 | invokedCreateOriginalImageUrlCount += 1 66 | invokedCreateOriginalImageUrlParameters = (path, ()) 67 | invokedCreateOriginalImageUrlParametersList.append((path, ())) 68 | return stubbedCreateOriginalImageUrlResult 69 | } 70 | 71 | var invokedCreateLogoimageUrl = false 72 | var invokedCreateLogoimageUrlCount = 0 73 | var invokedCreateLogoimageUrlParameters: (path: String?, Void)? 74 | var invokedCreateLogoimageUrlParametersList = [(path: String?, Void)]() 75 | var stubbedCreateLogoimageUrlResult: String! = "" 76 | 77 | func createLogoimageUrl(withPath path: String?) -> String { 78 | invokedCreateLogoimageUrl = true 79 | invokedCreateLogoimageUrlCount += 1 80 | invokedCreateLogoimageUrlParameters = (path, ()) 81 | invokedCreateLogoimageUrlParametersList.append((path, ())) 82 | return stubbedCreateLogoimageUrlResult 83 | } 84 | 85 | var invokedCreateRequestURL = false 86 | var invokedCreateRequestURLCount = 0 87 | var invokedCreateRequestURLParameters: (endpoint: String, pathVariables: [String]?, headerParams: [String: Any]?)? 88 | var invokedCreateRequestURLParametersList = [(endpoint: String, pathVariables: [String]?, headerParams: [String: Any]?)]() 89 | var stubbedCreateRequestURLResult: URL! = URL(string: AppConstants.shared.exampleImagePath)! 90 | 91 | func createRequestURL(_ endpoint: String, pathVariables: [String]?, headerParams: [String: Any]?) -> URL { 92 | invokedCreateRequestURL = true 93 | invokedCreateRequestURLCount += 1 94 | invokedCreateRequestURLParameters = (endpoint, pathVariables, headerParams) 95 | invokedCreateRequestURLParametersList.append((endpoint, pathVariables, headerParams)) 96 | return stubbedCreateRequestURLResult 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Modules/Detail/DetailViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailViewModel.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 11.11.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol DetailViewModelProtocol: ObservableObject { 11 | var mediaDetailValue: DetailViewModelValues { get } 12 | var isPageLoaded: Bool { get } 13 | } 14 | 15 | class DetailViewModel: DetailViewModelProtocol { 16 | @Published var mediaDetailValue: DetailViewModelValues = DetailViewModelValues() 17 | @Published var isPageLoaded: Bool = false 18 | 19 | var mediaId: Int 20 | var mediaType: MediaTypes 21 | private var manager: NetworkManagerProtocol! 22 | 23 | init(mediaId: Int, mediaType: MediaTypes, manager: NetworkManagerProtocol? = nil) { 24 | self.manager = manager ?? NetworkManager() 25 | self.mediaId = mediaId 26 | self.mediaType = mediaType 27 | handleMedia() 28 | } 29 | 30 | private func handleMedia() { 31 | let endpoint: String = mediaType == .tvShow ? ApiEndpoints.tvShowDetail.rawValue : ApiEndpoints.movieShowDetail.rawValue 32 | handleMediaDetail(endpoint) 33 | handleMediaActors(endpoint) 34 | handleMediaVideos(endpoint) 35 | handleRecommendedMedia(endpoint) 36 | } 37 | 38 | private func handleMediaDetail(_ endpoint: String) { 39 | let url = manager.createRequestURL(endpoint, pathVariables: [String(mediaId)], headerParams: nil) 40 | 41 | manager.apiRequest(endpoint: url, param: nil) { response in 42 | switch response { 43 | case .success(let data): 44 | guard let decodedData: Media = data.decodedModel() else { return } 45 | DispatchQueue.main.async { 46 | self.mediaDetailValue.mediaDetail = decodedData 47 | self.isPageLoaded = true 48 | } 49 | case .failure: 50 | print("err") 51 | } 52 | } 53 | } 54 | 55 | private func handleMediaActors(_ endpoint: String) { 56 | let actorsUrl = manager.createRequestURL(endpoint, pathVariables: [ 57 | String(mediaId), 58 | mediaType == .tvShow ? "aggregate_credits" : "credits" 59 | ], headerParams: nil) 60 | 61 | manager.apiRequest(endpoint: actorsUrl, param: nil) { response in 62 | switch response { 63 | case .success(let data): 64 | guard let decodedData: ActorList = data.decodedModel() else { return } 65 | DispatchQueue.main.async { 66 | self.mediaDetailValue.mediaActors = decodedData.actorList 67 | } 68 | case .failure: 69 | print("err") 70 | } 71 | } 72 | } 73 | 74 | private func handleMediaVideos(_ endpoint: String) { 75 | let videosUrl = manager.createRequestURL(endpoint, pathVariables: [ 76 | String(mediaId), 77 | "videos" 78 | ], headerParams: nil) 79 | 80 | manager.apiRequest(endpoint: videosUrl, param: nil) { response in 81 | switch response { 82 | case .success(let data): 83 | guard let decodedData: MediaVideoList = data.decodedModel() else { return } 84 | DispatchQueue.main.async { 85 | self.mediaDetailValue.mediaVideos = decodedData.mediaVideoList 86 | } 87 | case .failure: 88 | print("err") 89 | } 90 | } 91 | } 92 | 93 | private func handleRecommendedMedia(_ endpoint: String) { 94 | let recommendedURL = manager.createRequestURL(endpoint, pathVariables: [ 95 | String(mediaId), 96 | "recommendations" 97 | ], headerParams: nil) 98 | 99 | manager.apiRequest(endpoint: recommendedURL, param: nil) { response in 100 | switch response { 101 | case .success(let data): 102 | guard let decodedData: MediaList = data.decodedModel() else { return } 103 | DispatchQueue.main.async { 104 | self.mediaDetailValue.recommendedMedia = decodedData.mediaList 105 | } 106 | case .failure: 107 | print("err") 108 | } 109 | } 110 | } 111 | } 112 | 113 | struct DetailViewModelValues { 114 | var mediaDetail: Media 115 | var mediaActors: [Actor] 116 | var mediaVideos: [MediaVideos] 117 | var recommendedMedia: [Media] 118 | var mediaType: MediaTypes 119 | 120 | init() { 121 | self.mediaDetail = Media() 122 | self.mediaActors = [] 123 | self.mediaVideos = [] 124 | self.recommendedMedia = [] 125 | self.mediaType = .movie 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example/Modules/Detail/DetailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailView.swift 3 | // SwiftUI-MVVM-Example 4 | // 5 | // Created by Mehmet Ateş on 10.11.2022. 6 | // 7 | 8 | import SwiftUI 9 | import StickyAsyncImageSwiftUI 10 | 11 | struct DetailView: View where Model: DetailViewModelProtocol { 12 | @ObservedObject var viewModel: Model 13 | @Environment(\.openURL) var openURL 14 | @Environment(\.colorScheme) private var colorScheme 15 | 16 | var body: some View { 17 | if viewModel.isPageLoaded { 18 | createLoadedState(mediaValues: viewModel.mediaDetailValue) 19 | } else { 20 | createLoadedState(mediaValues: DetailViewModelValues()).redacted(reason: .placeholder) 21 | } 22 | } 23 | } 24 | 25 | struct DetailView_Previews: PreviewProvider { 26 | static var previews: some View { 27 | DetailView(viewModel: DetailViewModel(mediaId: 95403, mediaType: .tvShow)) 28 | } 29 | } 30 | 31 | // MARK: - View Components 32 | extension DetailView { 33 | private func createLoadedState(mediaValues: DetailViewModelValues) -> some View { 34 | ScrollView { 35 | StickyAsyncImageSwiftUI(url: URL(string: mediaValues.mediaDetail.originalBackdropImage), size: 50.0.responsizeW, coordinateSpace: "sticky", isGradientOn: true, linearGradient: overlayLinearGradient) 36 | createImageHeaderInformationStack(media: mediaValues.mediaDetail) 37 | LazyVStack(spacing: 3.0.responsizeW) { 38 | if !mediaValues.mediaDetail.overview.isEmpty { 39 | createOverViewStack(media: mediaValues.mediaDetail) 40 | } 41 | if !mediaValues.mediaActors.isEmpty { 42 | createActorsStack 43 | } 44 | if !mediaValues.mediaVideos.isEmpty { 45 | createVideosStack 46 | } 47 | if !mediaValues.recommendedMedia.isEmpty { 48 | youMayLikeStack 49 | } 50 | } 51 | }.coordinateSpace(name: "sticky") 52 | .ignoresSafeArea(edges: .top) 53 | } 54 | 55 | private func createImageHeaderInformationStack(media: Media) -> some View { 56 | VStack { 57 | HStack { 58 | AnimatedAsyncImageView(path: media.posterImage) 59 | .frame(width: 40.0.responsizeW, height: 60.0.responsizeW) 60 | .transformEffect(CGAffineTransform(1, 0, 0, 1, 0, -10.0.responsizeW)) 61 | VStack(alignment: .leading) { 62 | Text(media.title) 63 | .lineLimit(4) 64 | .font(.system(size: 6.responsizeW, weight: .bold)) 65 | Text(media.releaseYear) 66 | .font(.footnote) 67 | Spacer() 68 | if let mediaHomePage: URL = URL(string: media.mediaHomePage), let logoPath = media.mediaNetworks.first?.logoImagePath { 69 | Button { 70 | openURL(mediaHomePage) 71 | } label: { 72 | LogoImageView(imagePath: logoPath) 73 | } 74 | } 75 | } 76 | Spacer() 77 | }.padding(.horizontal) 78 | .frame(height: 40.0.responsizeW) 79 | } 80 | } 81 | 82 | private func createOverViewStack(media: Media) -> some View { 83 | CustomSectionView(title: "Overview") { 84 | Text(media.overview) 85 | .font(.caption) 86 | .padding(.horizontal) 87 | } 88 | } 89 | 90 | private var createActorsStack: some View { 91 | CustomSectionView(title: "Actors") { 92 | ScrollView(.horizontal, showsIndicators: false) { 93 | LazyHStack { 94 | ForEach(viewModel.mediaDetailValue.mediaActors) { mediaActor in 95 | ActorCardView(actor: mediaActor) 96 | } 97 | }.padding([.horizontal, .bottom]) 98 | } 99 | } 100 | } 101 | 102 | private var createVideosStack: some View { 103 | CustomSectionView(title: "Videos") { 104 | ScrollView(.horizontal, showsIndicators: false) { 105 | LazyHStack { 106 | ForEach(viewModel.mediaDetailValue.mediaVideos) { mediaVideo in 107 | YoutubeVideoView(mediaVideo.videoLink) 108 | } 109 | }.padding([.horizontal, .bottom]) 110 | } 111 | } 112 | } 113 | 114 | private var youMayLikeStack: some View { 115 | MediaListSection(mediaList: viewModel.mediaDetailValue.recommendedMedia, sectionTitle: "You May Like", titleFont: .title, mediaType: viewModel.mediaDetailValue.mediaType) 116 | } 117 | } 118 | 119 | // MARK: - Color Properties 120 | extension DetailView { 121 | private var overlayLinearGradient: LinearGradient { 122 | LinearGradient( 123 | colors: [backgroundColor.opacity(0.5), backgroundColor], 124 | startPoint: .top, 125 | endPoint: .bottom 126 | ) 127 | } 128 | private var backgroundColor: Color { colorScheme == .dark ? .black : .white } 129 | } 130 | -------------------------------------------------------------------------------- /SwiftUI-MVVM-Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C00232D0292A2B4C00FC82C2 /* Med_Exp_SearchViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00232CF292A2B4C00FC82C2 /* Med_Exp_SearchViewModelTests.swift */; }; 11 | C00232D2292A343800FC82C2 /* MedExp_DetailViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00232D1292A343800FC82C2 /* MedExp_DetailViewModelTests.swift */; }; 12 | C0085F482913EB98001C174A /* AnimatedAsyncImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0085F472913EB98001C174A /* AnimatedAsyncImageView.swift */; }; 13 | C0085F4C2913EE49001C174A /* Double + Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0085F4B2913EE49001C174A /* Double + Ext.swift */; }; 14 | C0085F4E2913EE72001C174A /* AppConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0085F4D2913EE72001C174A /* AppConstants.swift */; }; 15 | C010A56D291178CE00C971FB /* SwiftUI_MVVM_ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C010A56C291178CE00C971FB /* SwiftUI_MVVM_ExampleApp.swift */; }; 16 | C010A571291178CF00C971FB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C010A570291178CF00C971FB /* Assets.xcassets */; }; 17 | C010A574291178CF00C971FB /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C010A573291178CF00C971FB /* Preview Assets.xcassets */; }; 18 | C0239F05291CF15500FA36B8 /* StickyAsyncImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = C0239F04291CF15500FA36B8 /* StickyAsyncImageSwiftUI */; }; 19 | C0239F08291CF18E00FA36B8 /* DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0239F07291CF18E00FA36B8 /* DetailView.swift */; }; 20 | C0239F0A291D189300FA36B8 /* Genres.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0239F09291D189300FA36B8 /* Genres.swift */; }; 21 | C0239F0C291D18A300FA36B8 /* Networks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0239F0B291D18A300FA36B8 /* Networks.swift */; }; 22 | C0306E85291BAB1F00F448C8 /* View + Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0306E84291BAB1F00F448C8 /* View + Ext.swift */; }; 23 | C0306E87291BAEAA00F448C8 /* HeaderCarouselMediaList.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0306E86291BAEAA00F448C8 /* HeaderCarouselMediaList.swift */; }; 24 | C0306E89291BB17D00F448C8 /* SearchMovieCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0306E88291BB17D00F448C8 /* SearchMovieCardView.swift */; }; 25 | C03B6C7D2917ADC0008E5018 /* TabbarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03B6C7C2917ADC0008E5018 /* TabbarButton.swift */; }; 26 | C03B6C7F2917B1D8008E5018 /* TabbarRoot.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03B6C7E2917B1D8008E5018 /* TabbarRoot.swift */; }; 27 | C03B6C832917B1F6008E5018 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03B6C822917B1F6008E5018 /* HomeView.swift */; }; 28 | C03B6C852917B495008E5018 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03B6C842917B495008E5018 /* SearchView.swift */; }; 29 | C03B6C882917B610008E5018 /* MediaListSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03B6C872917B610008E5018 /* MediaListSection.swift */; }; 30 | C03B6C8A2917B78E008E5018 /* Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03B6C892917B78E008E5018 /* Media.swift */; }; 31 | C03B6C8C2917B795008E5018 /* MediaList.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03B6C8B2917B795008E5018 /* MediaList.swift */; }; 32 | C03B6C8F2917C816008E5018 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03B6C8E2917C816008E5018 /* HomeViewModel.swift */; }; 33 | C044397E291E4DF600EE354E /* ActorCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C044397D291E4DF600EE354E /* ActorCardView.swift */; }; 34 | C0443980291E4E1800EE354E /* CardProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C044397F291E4E1800EE354E /* CardProtocol.swift */; }; 35 | C04DF5472928D0C4006622A9 /* MedExp_HomeViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C04DF5462928D0C4006622A9 /* MedExp_HomeViewModelTests.swift */; }; 36 | C0892213291E79CB00524D9A /* Actor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0892212291E79CB00524D9A /* Actor.swift */; }; 37 | C0892215291E79FA00524D9A /* ActorList.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0892214291E79FA00524D9A /* ActorList.swift */; }; 38 | C0892217291E7A2F00524D9A /* Role.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0892216291E7A2F00524D9A /* Role.swift */; }; 39 | C0892219291E7FAC00524D9A /* DetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0892218291E7FAC00524D9A /* DetailViewModel.swift */; }; 40 | C089221B291E808300524D9A /* LogoImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C089221A291E808300524D9A /* LogoImageView.swift */; }; 41 | C089221D291E851B00524D9A /* CustomSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C089221C291E851B00524D9A /* CustomSectionView.swift */; }; 42 | C089221F291E86B700524D9A /* MediaVideos.swift in Sources */ = {isa = PBXBuildFile; fileRef = C089221E291E86B700524D9A /* MediaVideos.swift */; }; 43 | C0892221291E87C200524D9A /* MediaVideoList.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0892220291E87C200524D9A /* MediaVideoList.swift */; }; 44 | C0892223291E8B2400524D9A /* YoutubeVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0892222291E8B2400524D9A /* YoutubeVideoView.swift */; }; 45 | C094A956291509F500D2BB39 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C094A955291509F500D2BB39 /* NetworkManager.swift */; }; 46 | C0E239C02920FA0E0006D4F2 /* LazyNavigate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E239BF2920FA0D0006D4F2 /* LazyNavigate.swift */; }; 47 | C0EAD879291AC8750078DEE8 /* Data + Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0EAD878291AC8750078DEE8 /* Data + Ext.swift */; }; 48 | C0F0B59A29157CE600BA2499 /* AppEnvironments.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F0B59929157CE600BA2499 /* AppEnvironments.swift */; }; 49 | C0F628262913F7980020BB59 /* SwiftUIAnimatedRingCharts in Frameworks */ = {isa = PBXBuildFile; productRef = C0F628252913F7980020BB59 /* SwiftUIAnimatedRingCharts */; }; 50 | C0F7A4312928F91B004AB25D /* NetworkManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F7A4302928F91B004AB25D /* NetworkManagerMock.swift */; }; 51 | C0FA34A22912DCBB00885E73 /* MediaCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FA34A12912DCBB00885E73 /* MediaCardView.swift */; }; 52 | C0FC605C2918F48A00643C93 /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FC605B2918F48A00643C93 /* SearchViewModel.swift */; }; 53 | C0FC605E2918F52100643C93 /* RequestAbleMovieListProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FC605D2918F52100643C93 /* RequestAbleMovieListProtocol.swift */; }; 54 | /* End PBXBuildFile section */ 55 | 56 | /* Begin PBXContainerItemProxy section */ 57 | C07CFAAB2927EEA000DAD151 /* PBXContainerItemProxy */ = { 58 | isa = PBXContainerItemProxy; 59 | containerPortal = C010A561291178CE00C971FB /* Project object */; 60 | proxyType = 1; 61 | remoteGlobalIDString = C010A568291178CE00C971FB; 62 | remoteInfo = "SwiftUI-MVVM-Example"; 63 | }; 64 | /* End PBXContainerItemProxy section */ 65 | 66 | /* Begin PBXFileReference section */ 67 | C00232CF292A2B4C00FC82C2 /* Med_Exp_SearchViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Med_Exp_SearchViewModelTests.swift; sourceTree = ""; }; 68 | C00232D1292A343800FC82C2 /* MedExp_DetailViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedExp_DetailViewModelTests.swift; sourceTree = ""; }; 69 | C0085F472913EB98001C174A /* AnimatedAsyncImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedAsyncImageView.swift; sourceTree = ""; }; 70 | C0085F4B2913EE49001C174A /* Double + Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double + Ext.swift"; sourceTree = ""; }; 71 | C0085F4D2913EE72001C174A /* AppConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConstants.swift; sourceTree = ""; }; 72 | C010A569291178CE00C971FB /* SwiftUI-MVVM-Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SwiftUI-MVVM-Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 73 | C010A56C291178CE00C971FB /* SwiftUI_MVVM_ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUI_MVVM_ExampleApp.swift; sourceTree = ""; }; 74 | C010A570291178CF00C971FB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 75 | C010A573291178CF00C971FB /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 76 | C0239F07291CF18E00FA36B8 /* DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailView.swift; sourceTree = ""; }; 77 | C0239F09291D189300FA36B8 /* Genres.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Genres.swift; sourceTree = ""; }; 78 | C0239F0B291D18A300FA36B8 /* Networks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Networks.swift; sourceTree = ""; }; 79 | C0306E84291BAB1F00F448C8 /* View + Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View + Ext.swift"; sourceTree = ""; }; 80 | C0306E86291BAEAA00F448C8 /* HeaderCarouselMediaList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderCarouselMediaList.swift; sourceTree = ""; }; 81 | C0306E88291BB17D00F448C8 /* SearchMovieCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchMovieCardView.swift; sourceTree = ""; }; 82 | C03B6C7C2917ADC0008E5018 /* TabbarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabbarButton.swift; sourceTree = ""; }; 83 | C03B6C7E2917B1D8008E5018 /* TabbarRoot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabbarRoot.swift; sourceTree = ""; }; 84 | C03B6C822917B1F6008E5018 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 85 | C03B6C842917B495008E5018 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; 86 | C03B6C872917B610008E5018 /* MediaListSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaListSection.swift; sourceTree = ""; }; 87 | C03B6C892917B78E008E5018 /* Media.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Media.swift; sourceTree = ""; }; 88 | C03B6C8B2917B795008E5018 /* MediaList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaList.swift; sourceTree = ""; }; 89 | C03B6C8E2917C816008E5018 /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; 90 | C044397D291E4DF600EE354E /* ActorCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActorCardView.swift; sourceTree = ""; }; 91 | C044397F291E4E1800EE354E /* CardProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardProtocol.swift; sourceTree = ""; }; 92 | C04DF5462928D0C4006622A9 /* MedExp_HomeViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedExp_HomeViewModelTests.swift; sourceTree = ""; }; 93 | C07CFAA72927EEA000DAD151 /* SwiftUI-MVVM-ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SwiftUI-MVVM-ExampleTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 94 | C0892212291E79CB00524D9A /* Actor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Actor.swift; sourceTree = ""; }; 95 | C0892214291E79FA00524D9A /* ActorList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActorList.swift; sourceTree = ""; }; 96 | C0892216291E7A2F00524D9A /* Role.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Role.swift; sourceTree = ""; }; 97 | C0892218291E7FAC00524D9A /* DetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewModel.swift; sourceTree = ""; }; 98 | C089221A291E808300524D9A /* LogoImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoImageView.swift; sourceTree = ""; }; 99 | C089221C291E851B00524D9A /* CustomSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSectionView.swift; sourceTree = ""; }; 100 | C089221E291E86B700524D9A /* MediaVideos.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaVideos.swift; sourceTree = ""; }; 101 | C0892220291E87C200524D9A /* MediaVideoList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaVideoList.swift; sourceTree = ""; }; 102 | C0892222291E8B2400524D9A /* YoutubeVideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YoutubeVideoView.swift; sourceTree = ""; }; 103 | C094A955291509F500D2BB39 /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; 104 | C0E239BF2920FA0D0006D4F2 /* LazyNavigate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyNavigate.swift; sourceTree = ""; }; 105 | C0EAD878291AC8750078DEE8 /* Data + Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data + Ext.swift"; sourceTree = ""; }; 106 | C0F0B59929157CE600BA2499 /* AppEnvironments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppEnvironments.swift; sourceTree = ""; }; 107 | C0F7A4302928F91B004AB25D /* NetworkManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManagerMock.swift; sourceTree = ""; }; 108 | C0FA34A12912DCBB00885E73 /* MediaCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaCardView.swift; sourceTree = ""; }; 109 | C0FC605B2918F48A00643C93 /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; 110 | C0FC605D2918F52100643C93 /* RequestAbleMovieListProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestAbleMovieListProtocol.swift; sourceTree = ""; }; 111 | /* End PBXFileReference section */ 112 | 113 | /* Begin PBXFrameworksBuildPhase section */ 114 | C010A566291178CE00C971FB /* Frameworks */ = { 115 | isa = PBXFrameworksBuildPhase; 116 | buildActionMask = 2147483647; 117 | files = ( 118 | C0239F05291CF15500FA36B8 /* StickyAsyncImageSwiftUI in Frameworks */, 119 | C0F628262913F7980020BB59 /* SwiftUIAnimatedRingCharts in Frameworks */, 120 | ); 121 | runOnlyForDeploymentPostprocessing = 0; 122 | }; 123 | C07CFAA42927EEA000DAD151 /* Frameworks */ = { 124 | isa = PBXFrameworksBuildPhase; 125 | buildActionMask = 2147483647; 126 | files = ( 127 | ); 128 | runOnlyForDeploymentPostprocessing = 0; 129 | }; 130 | /* End PBXFrameworksBuildPhase section */ 131 | 132 | /* Begin PBXGroup section */ 133 | C0085F492913EE2F001C174A /* Utils */ = { 134 | isa = PBXGroup; 135 | children = ( 136 | C0085F4B2913EE49001C174A /* Double + Ext.swift */, 137 | C0085F4D2913EE72001C174A /* AppConstants.swift */, 138 | C0FC605D2918F52100643C93 /* RequestAbleMovieListProtocol.swift */, 139 | C0EAD878291AC8750078DEE8 /* Data + Ext.swift */, 140 | C0306E84291BAB1F00F448C8 /* View + Ext.swift */, 141 | C044397F291E4E1800EE354E /* CardProtocol.swift */, 142 | C0E239BF2920FA0D0006D4F2 /* LazyNavigate.swift */, 143 | ); 144 | path = Utils; 145 | sourceTree = ""; 146 | }; 147 | C010A560291178CE00C971FB = { 148 | isa = PBXGroup; 149 | children = ( 150 | C010A56B291178CE00C971FB /* SwiftUI-MVVM-Example */, 151 | C07CFAA82927EEA000DAD151 /* SwiftUI-MVVM-ExampleTests */, 152 | C010A56A291178CE00C971FB /* Products */, 153 | ); 154 | sourceTree = ""; 155 | }; 156 | C010A56A291178CE00C971FB /* Products */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | C010A569291178CE00C971FB /* SwiftUI-MVVM-Example.app */, 160 | C07CFAA72927EEA000DAD151 /* SwiftUI-MVVM-ExampleTests.xctest */, 161 | ); 162 | name = Products; 163 | sourceTree = ""; 164 | }; 165 | C010A56B291178CE00C971FB /* SwiftUI-MVVM-Example */ = { 166 | isa = PBXGroup; 167 | children = ( 168 | C03B6C862917B5D7008E5018 /* Models */, 169 | C03B6C7A2917AC09008E5018 /* Modules */, 170 | C0F0B59829157CD200BA2499 /* Core */, 171 | C094A954291509E600D2BB39 /* Network */, 172 | C0085F492913EE2F001C174A /* Utils */, 173 | C0FA34A02912DCAD00885E73 /* Components */, 174 | C010A570291178CF00C971FB /* Assets.xcassets */, 175 | C010A572291178CF00C971FB /* Preview Content */, 176 | ); 177 | path = "SwiftUI-MVVM-Example"; 178 | sourceTree = ""; 179 | }; 180 | C010A572291178CF00C971FB /* Preview Content */ = { 181 | isa = PBXGroup; 182 | children = ( 183 | C010A573291178CF00C971FB /* Preview Assets.xcassets */, 184 | ); 185 | path = "Preview Content"; 186 | sourceTree = ""; 187 | }; 188 | C0239F06291CF18200FA36B8 /* Detail */ = { 189 | isa = PBXGroup; 190 | children = ( 191 | C0239F07291CF18E00FA36B8 /* DetailView.swift */, 192 | C0892218291E7FAC00524D9A /* DetailViewModel.swift */, 193 | ); 194 | path = Detail; 195 | sourceTree = ""; 196 | }; 197 | C03B6C7A2917AC09008E5018 /* Modules */ = { 198 | isa = PBXGroup; 199 | children = ( 200 | C0239F06291CF18200FA36B8 /* Detail */, 201 | C03B6C812917B1EB008E5018 /* Search */, 202 | C03B6C802917B1E5008E5018 /* Home */, 203 | C03B6C7B2917AC34008E5018 /* Tabbar */, 204 | ); 205 | path = Modules; 206 | sourceTree = ""; 207 | }; 208 | C03B6C7B2917AC34008E5018 /* Tabbar */ = { 209 | isa = PBXGroup; 210 | children = ( 211 | C03B6C7C2917ADC0008E5018 /* TabbarButton.swift */, 212 | C03B6C7E2917B1D8008E5018 /* TabbarRoot.swift */, 213 | ); 214 | path = Tabbar; 215 | sourceTree = ""; 216 | }; 217 | C03B6C802917B1E5008E5018 /* Home */ = { 218 | isa = PBXGroup; 219 | children = ( 220 | C03B6C822917B1F6008E5018 /* HomeView.swift */, 221 | C03B6C8E2917C816008E5018 /* HomeViewModel.swift */, 222 | ); 223 | path = Home; 224 | sourceTree = ""; 225 | }; 226 | C03B6C812917B1EB008E5018 /* Search */ = { 227 | isa = PBXGroup; 228 | children = ( 229 | C03B6C842917B495008E5018 /* SearchView.swift */, 230 | C0FC605B2918F48A00643C93 /* SearchViewModel.swift */, 231 | ); 232 | path = Search; 233 | sourceTree = ""; 234 | }; 235 | C03B6C862917B5D7008E5018 /* Models */ = { 236 | isa = PBXGroup; 237 | children = ( 238 | C03B6C892917B78E008E5018 /* Media.swift */, 239 | C03B6C8B2917B795008E5018 /* MediaList.swift */, 240 | C0239F09291D189300FA36B8 /* Genres.swift */, 241 | C0239F0B291D18A300FA36B8 /* Networks.swift */, 242 | C0892212291E79CB00524D9A /* Actor.swift */, 243 | C0892214291E79FA00524D9A /* ActorList.swift */, 244 | C0892216291E7A2F00524D9A /* Role.swift */, 245 | C089221E291E86B700524D9A /* MediaVideos.swift */, 246 | C0892220291E87C200524D9A /* MediaVideoList.swift */, 247 | ); 248 | path = Models; 249 | sourceTree = ""; 250 | }; 251 | C07CFAA82927EEA000DAD151 /* SwiftUI-MVVM-ExampleTests */ = { 252 | isa = PBXGroup; 253 | children = ( 254 | C00232D1292A343800FC82C2 /* MedExp_DetailViewModelTests.swift */, 255 | C00232CF292A2B4C00FC82C2 /* Med_Exp_SearchViewModelTests.swift */, 256 | C04DF5462928D0C4006622A9 /* MedExp_HomeViewModelTests.swift */, 257 | ); 258 | path = "SwiftUI-MVVM-ExampleTests"; 259 | sourceTree = ""; 260 | }; 261 | C094A954291509E600D2BB39 /* Network */ = { 262 | isa = PBXGroup; 263 | children = ( 264 | C0F7A42F2928F90E004AB25D /* Mocks */, 265 | C094A955291509F500D2BB39 /* NetworkManager.swift */, 266 | ); 267 | path = Network; 268 | sourceTree = ""; 269 | }; 270 | C0F0B59829157CD200BA2499 /* Core */ = { 271 | isa = PBXGroup; 272 | children = ( 273 | C010A56C291178CE00C971FB /* SwiftUI_MVVM_ExampleApp.swift */, 274 | C0F0B59929157CE600BA2499 /* AppEnvironments.swift */, 275 | ); 276 | path = Core; 277 | sourceTree = ""; 278 | }; 279 | C0F7A42F2928F90E004AB25D /* Mocks */ = { 280 | isa = PBXGroup; 281 | children = ( 282 | C0F7A4302928F91B004AB25D /* NetworkManagerMock.swift */, 283 | ); 284 | path = Mocks; 285 | sourceTree = ""; 286 | }; 287 | C0FA34A02912DCAD00885E73 /* Components */ = { 288 | isa = PBXGroup; 289 | children = ( 290 | C0FA34A12912DCBB00885E73 /* MediaCardView.swift */, 291 | C0085F472913EB98001C174A /* AnimatedAsyncImageView.swift */, 292 | C03B6C872917B610008E5018 /* MediaListSection.swift */, 293 | C0306E86291BAEAA00F448C8 /* HeaderCarouselMediaList.swift */, 294 | C0306E88291BB17D00F448C8 /* SearchMovieCardView.swift */, 295 | C044397D291E4DF600EE354E /* ActorCardView.swift */, 296 | C089221A291E808300524D9A /* LogoImageView.swift */, 297 | C089221C291E851B00524D9A /* CustomSectionView.swift */, 298 | C0892222291E8B2400524D9A /* YoutubeVideoView.swift */, 299 | ); 300 | path = Components; 301 | sourceTree = ""; 302 | }; 303 | /* End PBXGroup section */ 304 | 305 | /* Begin PBXNativeTarget section */ 306 | C010A568291178CE00C971FB /* SwiftUI-MVVM-Example */ = { 307 | isa = PBXNativeTarget; 308 | buildConfigurationList = C010A577291178CF00C971FB /* Build configuration list for PBXNativeTarget "SwiftUI-MVVM-Example" */; 309 | buildPhases = ( 310 | C010A565291178CE00C971FB /* Sources */, 311 | C010A566291178CE00C971FB /* Frameworks */, 312 | C010A567291178CE00C971FB /* Resources */, 313 | ); 314 | buildRules = ( 315 | ); 316 | dependencies = ( 317 | ); 318 | name = "SwiftUI-MVVM-Example"; 319 | packageProductDependencies = ( 320 | C0F628252913F7980020BB59 /* SwiftUIAnimatedRingCharts */, 321 | C0239F04291CF15500FA36B8 /* StickyAsyncImageSwiftUI */, 322 | ); 323 | productName = "SwiftUI-MVVM-Example"; 324 | productReference = C010A569291178CE00C971FB /* SwiftUI-MVVM-Example.app */; 325 | productType = "com.apple.product-type.application"; 326 | }; 327 | C07CFAA62927EEA000DAD151 /* SwiftUI-MVVM-ExampleTests */ = { 328 | isa = PBXNativeTarget; 329 | buildConfigurationList = C07CFAAF2927EEA000DAD151 /* Build configuration list for PBXNativeTarget "SwiftUI-MVVM-ExampleTests" */; 330 | buildPhases = ( 331 | C07CFAA32927EEA000DAD151 /* Sources */, 332 | C07CFAA42927EEA000DAD151 /* Frameworks */, 333 | C07CFAA52927EEA000DAD151 /* Resources */, 334 | ); 335 | buildRules = ( 336 | ); 337 | dependencies = ( 338 | C07CFAAC2927EEA000DAD151 /* PBXTargetDependency */, 339 | ); 340 | name = "SwiftUI-MVVM-ExampleTests"; 341 | productName = "SwiftUI-MVVM-ExampleTests"; 342 | productReference = C07CFAA72927EEA000DAD151 /* SwiftUI-MVVM-ExampleTests.xctest */; 343 | productType = "com.apple.product-type.bundle.unit-test"; 344 | }; 345 | /* End PBXNativeTarget section */ 346 | 347 | /* Begin PBXProject section */ 348 | C010A561291178CE00C971FB /* Project object */ = { 349 | isa = PBXProject; 350 | attributes = { 351 | BuildIndependentTargetsInParallel = 1; 352 | LastSwiftUpdateCheck = 1410; 353 | LastUpgradeCheck = 1400; 354 | TargetAttributes = { 355 | C010A568291178CE00C971FB = { 356 | CreatedOnToolsVersion = 14.0.1; 357 | }; 358 | C07CFAA62927EEA000DAD151 = { 359 | CreatedOnToolsVersion = 14.1; 360 | TestTargetID = C010A568291178CE00C971FB; 361 | }; 362 | }; 363 | }; 364 | buildConfigurationList = C010A564291178CE00C971FB /* Build configuration list for PBXProject "SwiftUI-MVVM-Example" */; 365 | compatibilityVersion = "Xcode 14.0"; 366 | developmentRegion = en; 367 | hasScannedForEncodings = 0; 368 | knownRegions = ( 369 | en, 370 | Base, 371 | ); 372 | mainGroup = C010A560291178CE00C971FB; 373 | packageReferences = ( 374 | C0F628242913F7980020BB59 /* XCRemoteSwiftPackageReference "SwiftUIAnimatedRingCharts" */, 375 | C0239F03291CF15500FA36B8 /* XCRemoteSwiftPackageReference "StickyAsyncImageSwiftUI" */, 376 | ); 377 | productRefGroup = C010A56A291178CE00C971FB /* Products */; 378 | projectDirPath = ""; 379 | projectRoot = ""; 380 | targets = ( 381 | C010A568291178CE00C971FB /* SwiftUI-MVVM-Example */, 382 | C07CFAA62927EEA000DAD151 /* SwiftUI-MVVM-ExampleTests */, 383 | ); 384 | }; 385 | /* End PBXProject section */ 386 | 387 | /* Begin PBXResourcesBuildPhase section */ 388 | C010A567291178CE00C971FB /* Resources */ = { 389 | isa = PBXResourcesBuildPhase; 390 | buildActionMask = 2147483647; 391 | files = ( 392 | C010A574291178CF00C971FB /* Preview Assets.xcassets in Resources */, 393 | C010A571291178CF00C971FB /* Assets.xcassets in Resources */, 394 | ); 395 | runOnlyForDeploymentPostprocessing = 0; 396 | }; 397 | C07CFAA52927EEA000DAD151 /* Resources */ = { 398 | isa = PBXResourcesBuildPhase; 399 | buildActionMask = 2147483647; 400 | files = ( 401 | ); 402 | runOnlyForDeploymentPostprocessing = 0; 403 | }; 404 | /* End PBXResourcesBuildPhase section */ 405 | 406 | /* Begin PBXSourcesBuildPhase section */ 407 | C010A565291178CE00C971FB /* Sources */ = { 408 | isa = PBXSourcesBuildPhase; 409 | buildActionMask = 2147483647; 410 | files = ( 411 | C03B6C8A2917B78E008E5018 /* Media.swift in Sources */, 412 | C0085F4C2913EE49001C174A /* Double + Ext.swift in Sources */, 413 | C0F7A4312928F91B004AB25D /* NetworkManagerMock.swift in Sources */, 414 | C0FC605C2918F48A00643C93 /* SearchViewModel.swift in Sources */, 415 | C0EAD879291AC8750078DEE8 /* Data + Ext.swift in Sources */, 416 | C0892221291E87C200524D9A /* MediaVideoList.swift in Sources */, 417 | C0F0B59A29157CE600BA2499 /* AppEnvironments.swift in Sources */, 418 | C0239F08291CF18E00FA36B8 /* DetailView.swift in Sources */, 419 | C0892223291E8B2400524D9A /* YoutubeVideoView.swift in Sources */, 420 | C03B6C8C2917B795008E5018 /* MediaList.swift in Sources */, 421 | C0443980291E4E1800EE354E /* CardProtocol.swift in Sources */, 422 | C0FC605E2918F52100643C93 /* RequestAbleMovieListProtocol.swift in Sources */, 423 | C0239F0A291D189300FA36B8 /* Genres.swift in Sources */, 424 | C0085F4E2913EE72001C174A /* AppConstants.swift in Sources */, 425 | C0892215291E79FA00524D9A /* ActorList.swift in Sources */, 426 | C089221F291E86B700524D9A /* MediaVideos.swift in Sources */, 427 | C0306E89291BB17D00F448C8 /* SearchMovieCardView.swift in Sources */, 428 | C0E239C02920FA0E0006D4F2 /* LazyNavigate.swift in Sources */, 429 | C0892219291E7FAC00524D9A /* DetailViewModel.swift in Sources */, 430 | C0892213291E79CB00524D9A /* Actor.swift in Sources */, 431 | C044397E291E4DF600EE354E /* ActorCardView.swift in Sources */, 432 | C03B6C882917B610008E5018 /* MediaListSection.swift in Sources */, 433 | C03B6C852917B495008E5018 /* SearchView.swift in Sources */, 434 | C094A956291509F500D2BB39 /* NetworkManager.swift in Sources */, 435 | C0306E87291BAEAA00F448C8 /* HeaderCarouselMediaList.swift in Sources */, 436 | C089221B291E808300524D9A /* LogoImageView.swift in Sources */, 437 | C03B6C832917B1F6008E5018 /* HomeView.swift in Sources */, 438 | C03B6C7D2917ADC0008E5018 /* TabbarButton.swift in Sources */, 439 | C0FA34A22912DCBB00885E73 /* MediaCardView.swift in Sources */, 440 | C03B6C7F2917B1D8008E5018 /* TabbarRoot.swift in Sources */, 441 | C0306E85291BAB1F00F448C8 /* View + Ext.swift in Sources */, 442 | C0085F482913EB98001C174A /* AnimatedAsyncImageView.swift in Sources */, 443 | C03B6C8F2917C816008E5018 /* HomeViewModel.swift in Sources */, 444 | C0892217291E7A2F00524D9A /* Role.swift in Sources */, 445 | C089221D291E851B00524D9A /* CustomSectionView.swift in Sources */, 446 | C0239F0C291D18A300FA36B8 /* Networks.swift in Sources */, 447 | C010A56D291178CE00C971FB /* SwiftUI_MVVM_ExampleApp.swift in Sources */, 448 | ); 449 | runOnlyForDeploymentPostprocessing = 0; 450 | }; 451 | C07CFAA32927EEA000DAD151 /* Sources */ = { 452 | isa = PBXSourcesBuildPhase; 453 | buildActionMask = 2147483647; 454 | files = ( 455 | C00232D2292A343800FC82C2 /* MedExp_DetailViewModelTests.swift in Sources */, 456 | C04DF5472928D0C4006622A9 /* MedExp_HomeViewModelTests.swift in Sources */, 457 | C00232D0292A2B4C00FC82C2 /* Med_Exp_SearchViewModelTests.swift in Sources */, 458 | ); 459 | runOnlyForDeploymentPostprocessing = 0; 460 | }; 461 | /* End PBXSourcesBuildPhase section */ 462 | 463 | /* Begin PBXTargetDependency section */ 464 | C07CFAAC2927EEA000DAD151 /* PBXTargetDependency */ = { 465 | isa = PBXTargetDependency; 466 | target = C010A568291178CE00C971FB /* SwiftUI-MVVM-Example */; 467 | targetProxy = C07CFAAB2927EEA000DAD151 /* PBXContainerItemProxy */; 468 | }; 469 | /* End PBXTargetDependency section */ 470 | 471 | /* Begin XCBuildConfiguration section */ 472 | C010A575291178CF00C971FB /* Debug */ = { 473 | isa = XCBuildConfiguration; 474 | buildSettings = { 475 | ALWAYS_SEARCH_USER_PATHS = NO; 476 | CLANG_ANALYZER_NONNULL = YES; 477 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 478 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 479 | CLANG_ENABLE_MODULES = YES; 480 | CLANG_ENABLE_OBJC_ARC = YES; 481 | CLANG_ENABLE_OBJC_WEAK = YES; 482 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 483 | CLANG_WARN_BOOL_CONVERSION = YES; 484 | CLANG_WARN_COMMA = YES; 485 | CLANG_WARN_CONSTANT_CONVERSION = YES; 486 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 487 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 488 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 489 | CLANG_WARN_EMPTY_BODY = YES; 490 | CLANG_WARN_ENUM_CONVERSION = YES; 491 | CLANG_WARN_INFINITE_RECURSION = YES; 492 | CLANG_WARN_INT_CONVERSION = YES; 493 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 494 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 495 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 496 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 497 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 498 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 499 | CLANG_WARN_STRICT_PROTOTYPES = YES; 500 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 501 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 502 | CLANG_WARN_UNREACHABLE_CODE = YES; 503 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 504 | COPY_PHASE_STRIP = NO; 505 | DEBUG_INFORMATION_FORMAT = dwarf; 506 | ENABLE_STRICT_OBJC_MSGSEND = YES; 507 | ENABLE_TESTABILITY = YES; 508 | GCC_C_LANGUAGE_STANDARD = gnu11; 509 | GCC_DYNAMIC_NO_PIC = NO; 510 | GCC_NO_COMMON_BLOCKS = YES; 511 | GCC_OPTIMIZATION_LEVEL = 0; 512 | GCC_PREPROCESSOR_DEFINITIONS = ( 513 | "DEBUG=1", 514 | "$(inherited)", 515 | ); 516 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 517 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 518 | GCC_WARN_UNDECLARED_SELECTOR = YES; 519 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 520 | GCC_WARN_UNUSED_FUNCTION = YES; 521 | GCC_WARN_UNUSED_VARIABLE = YES; 522 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 523 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 524 | MTL_FAST_MATH = YES; 525 | ONLY_ACTIVE_ARCH = YES; 526 | SDKROOT = iphoneos; 527 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 528 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 529 | }; 530 | name = Debug; 531 | }; 532 | C010A576291178CF00C971FB /* Release */ = { 533 | isa = XCBuildConfiguration; 534 | buildSettings = { 535 | ALWAYS_SEARCH_USER_PATHS = NO; 536 | CLANG_ANALYZER_NONNULL = YES; 537 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 538 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 539 | CLANG_ENABLE_MODULES = YES; 540 | CLANG_ENABLE_OBJC_ARC = YES; 541 | CLANG_ENABLE_OBJC_WEAK = YES; 542 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 543 | CLANG_WARN_BOOL_CONVERSION = YES; 544 | CLANG_WARN_COMMA = YES; 545 | CLANG_WARN_CONSTANT_CONVERSION = YES; 546 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 547 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 548 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 549 | CLANG_WARN_EMPTY_BODY = YES; 550 | CLANG_WARN_ENUM_CONVERSION = YES; 551 | CLANG_WARN_INFINITE_RECURSION = YES; 552 | CLANG_WARN_INT_CONVERSION = YES; 553 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 554 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 555 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 556 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 557 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 558 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 559 | CLANG_WARN_STRICT_PROTOTYPES = YES; 560 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 561 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 562 | CLANG_WARN_UNREACHABLE_CODE = YES; 563 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 564 | COPY_PHASE_STRIP = NO; 565 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 566 | ENABLE_NS_ASSERTIONS = NO; 567 | ENABLE_STRICT_OBJC_MSGSEND = YES; 568 | GCC_C_LANGUAGE_STANDARD = gnu11; 569 | GCC_NO_COMMON_BLOCKS = YES; 570 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 571 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 572 | GCC_WARN_UNDECLARED_SELECTOR = YES; 573 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 574 | GCC_WARN_UNUSED_FUNCTION = YES; 575 | GCC_WARN_UNUSED_VARIABLE = YES; 576 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 577 | MTL_ENABLE_DEBUG_INFO = NO; 578 | MTL_FAST_MATH = YES; 579 | SDKROOT = iphoneos; 580 | SWIFT_COMPILATION_MODE = wholemodule; 581 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 582 | VALIDATE_PRODUCT = YES; 583 | }; 584 | name = Release; 585 | }; 586 | C010A578291178CF00C971FB /* Debug */ = { 587 | isa = XCBuildConfiguration; 588 | buildSettings = { 589 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 590 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 591 | CODE_SIGN_STYLE = Automatic; 592 | CURRENT_PROJECT_VERSION = 1; 593 | DEVELOPMENT_ASSET_PATHS = "\"SwiftUI-MVVM-Example/Preview Content\""; 594 | DEVELOPMENT_TEAM = 6PALB6K8YY; 595 | ENABLE_PREVIEWS = YES; 596 | GENERATE_INFOPLIST_FILE = YES; 597 | INFOPLIST_KEY_CFBundleDisplayName = MedExp; 598 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.lifestyle"; 599 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 600 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 601 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 602 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; 603 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; 604 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 605 | LD_RUNPATH_SEARCH_PATHS = ( 606 | "$(inherited)", 607 | "@executable_path/Frameworks", 608 | ); 609 | MARKETING_VERSION = 1.0; 610 | PRODUCT_BUNDLE_IDENTIFIER = "com.devmehmetates.SwiftUI-MVVM-Example"; 611 | PRODUCT_NAME = "$(TARGET_NAME)"; 612 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 613 | SUPPORTS_MACCATALYST = NO; 614 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 615 | SWIFT_EMIT_LOC_STRINGS = YES; 616 | SWIFT_VERSION = 5.0; 617 | TARGETED_DEVICE_FAMILY = 1; 618 | }; 619 | name = Debug; 620 | }; 621 | C010A579291178CF00C971FB /* Release */ = { 622 | isa = XCBuildConfiguration; 623 | buildSettings = { 624 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 625 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 626 | CODE_SIGN_STYLE = Automatic; 627 | CURRENT_PROJECT_VERSION = 1; 628 | DEVELOPMENT_ASSET_PATHS = "\"SwiftUI-MVVM-Example/Preview Content\""; 629 | DEVELOPMENT_TEAM = 6PALB6K8YY; 630 | ENABLE_PREVIEWS = YES; 631 | GENERATE_INFOPLIST_FILE = YES; 632 | INFOPLIST_KEY_CFBundleDisplayName = MedExp; 633 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.lifestyle"; 634 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 635 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 636 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 637 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; 638 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; 639 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 640 | LD_RUNPATH_SEARCH_PATHS = ( 641 | "$(inherited)", 642 | "@executable_path/Frameworks", 643 | ); 644 | MARKETING_VERSION = 1.0; 645 | PRODUCT_BUNDLE_IDENTIFIER = "com.devmehmetates.SwiftUI-MVVM-Example"; 646 | PRODUCT_NAME = "$(TARGET_NAME)"; 647 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 648 | SUPPORTS_MACCATALYST = NO; 649 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 650 | SWIFT_EMIT_LOC_STRINGS = YES; 651 | SWIFT_VERSION = 5.0; 652 | TARGETED_DEVICE_FAMILY = 1; 653 | }; 654 | name = Release; 655 | }; 656 | C07CFAAD2927EEA000DAD151 /* Debug */ = { 657 | isa = XCBuildConfiguration; 658 | buildSettings = { 659 | BUNDLE_LOADER = "$(TEST_HOST)"; 660 | CODE_SIGN_STYLE = Automatic; 661 | CURRENT_PROJECT_VERSION = 1; 662 | DEVELOPMENT_TEAM = 6PALB6K8YY; 663 | GENERATE_INFOPLIST_FILE = YES; 664 | IPHONEOS_DEPLOYMENT_TARGET = 16.1; 665 | MACOSX_DEPLOYMENT_TARGET = 13.0; 666 | MARKETING_VERSION = 1.0; 667 | PRODUCT_BUNDLE_IDENTIFIER = "com.devmehmetates.SwiftUI-MVVM-ExampleTests"; 668 | PRODUCT_NAME = "$(TARGET_NAME)"; 669 | SDKROOT = auto; 670 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 671 | SWIFT_EMIT_LOC_STRINGS = NO; 672 | SWIFT_VERSION = 5.0; 673 | TARGETED_DEVICE_FAMILY = "1,2"; 674 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftUI-MVVM-Example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SwiftUI-MVVM-Example"; 675 | }; 676 | name = Debug; 677 | }; 678 | C07CFAAE2927EEA000DAD151 /* Release */ = { 679 | isa = XCBuildConfiguration; 680 | buildSettings = { 681 | BUNDLE_LOADER = "$(TEST_HOST)"; 682 | CODE_SIGN_STYLE = Automatic; 683 | CURRENT_PROJECT_VERSION = 1; 684 | DEVELOPMENT_TEAM = 6PALB6K8YY; 685 | GENERATE_INFOPLIST_FILE = YES; 686 | IPHONEOS_DEPLOYMENT_TARGET = 16.1; 687 | MACOSX_DEPLOYMENT_TARGET = 13.0; 688 | MARKETING_VERSION = 1.0; 689 | PRODUCT_BUNDLE_IDENTIFIER = "com.devmehmetates.SwiftUI-MVVM-ExampleTests"; 690 | PRODUCT_NAME = "$(TARGET_NAME)"; 691 | SDKROOT = auto; 692 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 693 | SWIFT_EMIT_LOC_STRINGS = NO; 694 | SWIFT_VERSION = 5.0; 695 | TARGETED_DEVICE_FAMILY = "1,2"; 696 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftUI-MVVM-Example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SwiftUI-MVVM-Example"; 697 | }; 698 | name = Release; 699 | }; 700 | /* End XCBuildConfiguration section */ 701 | 702 | /* Begin XCConfigurationList section */ 703 | C010A564291178CE00C971FB /* Build configuration list for PBXProject "SwiftUI-MVVM-Example" */ = { 704 | isa = XCConfigurationList; 705 | buildConfigurations = ( 706 | C010A575291178CF00C971FB /* Debug */, 707 | C010A576291178CF00C971FB /* Release */, 708 | ); 709 | defaultConfigurationIsVisible = 0; 710 | defaultConfigurationName = Release; 711 | }; 712 | C010A577291178CF00C971FB /* Build configuration list for PBXNativeTarget "SwiftUI-MVVM-Example" */ = { 713 | isa = XCConfigurationList; 714 | buildConfigurations = ( 715 | C010A578291178CF00C971FB /* Debug */, 716 | C010A579291178CF00C971FB /* Release */, 717 | ); 718 | defaultConfigurationIsVisible = 0; 719 | defaultConfigurationName = Release; 720 | }; 721 | C07CFAAF2927EEA000DAD151 /* Build configuration list for PBXNativeTarget "SwiftUI-MVVM-ExampleTests" */ = { 722 | isa = XCConfigurationList; 723 | buildConfigurations = ( 724 | C07CFAAD2927EEA000DAD151 /* Debug */, 725 | C07CFAAE2927EEA000DAD151 /* Release */, 726 | ); 727 | defaultConfigurationIsVisible = 0; 728 | defaultConfigurationName = Release; 729 | }; 730 | /* End XCConfigurationList section */ 731 | 732 | /* Begin XCRemoteSwiftPackageReference section */ 733 | C0239F03291CF15500FA36B8 /* XCRemoteSwiftPackageReference "StickyAsyncImageSwiftUI" */ = { 734 | isa = XCRemoteSwiftPackageReference; 735 | repositoryURL = "https://github.com/devmehmetates/StickyAsyncImageSwiftUI.git"; 736 | requirement = { 737 | branch = main; 738 | kind = branch; 739 | }; 740 | }; 741 | C0F628242913F7980020BB59 /* XCRemoteSwiftPackageReference "SwiftUIAnimatedRingCharts" */ = { 742 | isa = XCRemoteSwiftPackageReference; 743 | repositoryURL = "https://github.com/devmehmetates/SwiftUIAnimatedRingCharts.git"; 744 | requirement = { 745 | branch = main; 746 | kind = branch; 747 | }; 748 | }; 749 | /* End XCRemoteSwiftPackageReference section */ 750 | 751 | /* Begin XCSwiftPackageProductDependency section */ 752 | C0239F04291CF15500FA36B8 /* StickyAsyncImageSwiftUI */ = { 753 | isa = XCSwiftPackageProductDependency; 754 | package = C0239F03291CF15500FA36B8 /* XCRemoteSwiftPackageReference "StickyAsyncImageSwiftUI" */; 755 | productName = StickyAsyncImageSwiftUI; 756 | }; 757 | C0F628252913F7980020BB59 /* SwiftUIAnimatedRingCharts */ = { 758 | isa = XCSwiftPackageProductDependency; 759 | package = C0F628242913F7980020BB59 /* XCRemoteSwiftPackageReference "SwiftUIAnimatedRingCharts" */; 760 | productName = SwiftUIAnimatedRingCharts; 761 | }; 762 | /* End XCSwiftPackageProductDependency section */ 763 | }; 764 | rootObject = C010A561291178CE00C971FB /* Project object */; 765 | } 766 | --------------------------------------------------------------------------------