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