美文网首页
SwiftUI框架详细解析 (十七) —— 基于SwiftUI简

SwiftUI框架详细解析 (十七) —— 基于SwiftUI简

作者: 刀客传奇 | 来源:发表于2021-01-02 11:53 被阅读0次

    版本记录

    版本号 时间
    V1.0 2021.01.02 星期六

    前言

    今天翻阅苹果的API文档,发现多了一个框架SwiftUI,这里我们就一起来看一下这个框架。感兴趣的看下面几篇文章。
    1. SwiftUI框架详细解析 (一) —— 基本概览(一)
    2. SwiftUI框架详细解析 (二) —— 基于SwiftUI的闪屏页的创建(一)
    3. SwiftUI框架详细解析 (三) —— 基于SwiftUI的闪屏页的创建(二)
    4. SwiftUI框架详细解析 (四) —— 使用SwiftUI进行苹果登录(一)
    5. SwiftUI框架详细解析 (五) —— 使用SwiftUI进行苹果登录(二)
    6. SwiftUI框架详细解析 (六) —— 基于SwiftUI的导航的实现(一)
    7. SwiftUI框架详细解析 (七) —— 基于SwiftUI的导航的实现(二)
    8. SwiftUI框架详细解析 (八) —— 基于SwiftUI的动画的实现(一)
    9. SwiftUI框架详细解析 (九) —— 基于SwiftUI的动画的实现(二)
    10. SwiftUI框架详细解析 (十) —— 基于SwiftUI构建各种自定义图表(一)
    11. SwiftUI框架详细解析 (十一) —— 基于SwiftUI构建各种自定义图表(二)
    12. SwiftUI框架详细解析 (十二) —— 基于SwiftUI创建Mind-Map UI(一)
    13. SwiftUI框架详细解析 (十三) —— 基于SwiftUI创建Mind-Map UI(二)
    14. SwiftUI框架详细解析 (十四) —— 基于Firebase Cloud Firestore的SwiftUI iOS程序的持久性添加(一)
    15. SwiftUI框架详细解析 (十五) —— 基于Firebase Cloud Firestore的SwiftUI iOS程序的持久性添加(二)
    16. SwiftUI框架详细解析 (十六) —— 基于SwiftUI简单App的Dependency Injection应用(一)

    源码

    1. Swift

    首先看下工程组织结构

    下面就是源码了

    1. PreferencesStore.swift
    
    import Combine
    import Foundation
    
    protocol PreferencesStoreProtocol: ObservableObject {
      var friendsListPreference: PrivacyLevel { get set }
      var photosPreference: PrivacyLevel { get set }
      var feedPreference: PrivacyLevel { get set }
      var videoCallsPreference: PrivacyLevel { get set }
      var messagePreference: PrivacyLevel { get set }
      func resetPreferences()
    }
    
    final class PreferencesStore: PreferencesStoreProtocol {
      @Published var friendsListPreference = value(for: .friends, defaultValue: .friend) {
        didSet {
          set(value: photosPreference, for: .friends)
        }
      }
    
      @Published var photosPreference = value(for: .photos, defaultValue: .friend) {
        didSet {
          set(value: photosPreference, for: .photos)
        }
      }
    
      @Published var feedPreference = value(for: .feed, defaultValue: .friend) {
        didSet {
          set(value: feedPreference, for: .feed)
        }
      }
    
      @Published var videoCallsPreference = value(for: .videoCall, defaultValue: .closeFriend) {
        didSet {
          set(value: videoCallsPreference, for: .videoCall)
        }
      }
    
      @Published var messagePreference: PrivacyLevel = value(for: .message, defaultValue: .friend) {
        didSet {
          set(value: messagePreference, for: .message)
        }
      }
    
      func resetPreferences() {
        let defaults = UserDefaults.standard
        PrivacySetting.allCases.forEach { setting in
          defaults.removeObject(forKey: setting.rawValue)
        }
      }
    
      private static func value(for key: PrivacySetting, defaultValue: PrivacyLevel) -> PrivacyLevel {
        let value = UserDefaults.standard.string(forKey: key.rawValue) ?? ""
        return PrivacyLevel.from(string: value) ?? defaultValue
      }
    
      private func set(value: PrivacyLevel, for key: PrivacySetting) {
        UserDefaults.standard.setValue(value.title, forKey: key.rawValue)
      }
    }
    
    2. PrivacyLevel.swift
    
    import Foundation
    
    enum PrivacyLevel: Comparable {
      case everyone, friend, closeFriend
    
      var title: String {
        switch self {
        case .everyone:
          return "Everyone"
        case .friend:
          return "Friends only"
        case .closeFriend:
          return "Close friends only"
        }
      }
    
      static func from(string: String) -> PrivacyLevel? {
        switch string {
        case everyone.title:
          return everyone
        case friend.title:
          return friend
        case closeFriend.title:
          return closeFriend
        default:
          return nil
        }
      }
    }
    
    3. UserPreferencesView.swift
    
    import SwiftUI
    import Combine
    
    struct UserPreferencesView<Store>: View where Store: PreferencesStoreProtocol {
      private var store: Store
    
      // swiftlint:disable:next force_unwrapping
      init(store: Store = DIContainer.shared.resolve(type: Store.self)!) {
        self.store = store
      }
    
      var body: some View {
        NavigationView {
          VStack {
            PreferenceView(title: .photos, value: store.photosPreference) { value in
              store.photosPreference = value
            }
            PreferenceView(title: .friends, value: store.friendsListPreference) { value in
              store.friendsListPreference = value
            }
            PreferenceView(title: .feed, value: store.feedPreference) { value in
              store.feedPreference = value
            }
            PreferenceView(title: .videoCall, value: store.videoCallsPreference) { value in
              store.videoCallsPreference = value
            }
            PreferenceView(title: .message, value: store.messagePreference) { value in
              store.messagePreference = value
            }
            Spacer()
          }
        }.navigationBarTitle("Privacy preferences")
      }
    }
    
    struct PreferenceView: View {
      private let title: PrivacySetting
      private let value: PrivacyLevel
      private let onPreferenceUpdated: (PrivacyLevel) -> Void
    
      init(title: PrivacySetting, value: PrivacyLevel, onPreferenceUpdated: @escaping (PrivacyLevel) -> Void) {
        self.title = title
        self.value = value
        self.onPreferenceUpdated = onPreferenceUpdated
      }
    
      var body: some View {
        HStack {
          Text(title.rawValue).font(.body)
          Spacer()
          PreferenceMenu(title: value.title, onPreferenceUpdated: onPreferenceUpdated)
        }.padding()
      }
    }
    
    struct PreferenceMenu: View {
      @State var title: String
      private let onPreferenceUpdated: (PrivacyLevel) -> Void
    
      init(title: String, onPreferenceUpdated: @escaping (PrivacyLevel) -> Void) {
        _title = State<String>(initialValue: title)
        self.onPreferenceUpdated = onPreferenceUpdated
      }
    
      var body: some View {
        Menu(title) {
          Button(PrivacyLevel.closeFriend.title) {
            onPreferenceUpdated(PrivacyLevel.closeFriend)
            title = PrivacyLevel.closeFriend.title
          }
          Button(PrivacyLevel.friend.title) {
            onPreferenceUpdated(PrivacyLevel.friend)
            title = PrivacyLevel.friend.title
          }
          Button(PrivacyLevel.everyone.title) {
            onPreferenceUpdated(PrivacyLevel.everyone)
            title = PrivacyLevel.everyone.title
          }
        }
      }
    }
    
    struct UserPreferencesView_Previews: PreviewProvider {
      static var previews: some View {
        UserPreferencesView(store: PreferencesStore())
      }
    }
    
    4. ProfileView.swift
    
    import SwiftUI
    
    struct ProfileView<ContentProvider>: View where ContentProvider: ProfileContentProviderProtocol {
      private let user: User
      @ObservedObject private var provider: ContentProvider
    
      init(
        // swiftlint:disable force_unwrapping
        provider: ContentProvider = DIContainer.shared.resolve(type: ContentProvider.self)!,
        user: User = DIContainer.shared.resolve(type: User.self)!
        // swiftlint:enable force_unwrapping
      ) {
        self.provider = provider
        self.user = user
      }
    
      var body: some View {
        NavigationView {
          ScrollView(.vertical, showsIndicators: true) {
            VStack {
              ProfileHeaderView(
                user: user,
                canSendMessage: provider.canSendMessage,
                canStartVideoChat: provider.canStartVideoChat
              )
              provider.friendsView
              provider.photosView
              provider.feedView
            }
          }
          .navigationTitle("Profile")
          .navigationBarItems(trailing: Button(action: {}) {
            NavigationLink(destination: UserPreferencesView<PreferencesStore>()) {
              Image(systemName: "gear")
            }
          })
        }
      }
    }
    
    struct ProfileView_Previews: PreviewProvider {
      static var previews: some View {
        typealias Provider = ProfileContentProvider<PreferencesStore>
        let container = DIContainer.shared
        container.register(type: PrivacyLevel.self, component: PrivacyLevel.friend)
        container.register(type: User.self, component: Mock.user())
        container.register(type: PreferencesStore.self, component: PreferencesStore())
        container.register(type: Provider.self, component: Provider())
        return ProfileView<Provider>()
      }
    }
    
    // MARK: - Profile views
    
    struct PhotosView: View {
      private let photos: [String]
    
      init(photos: [String]) {
        self.photos = photos
      }
    
      var body: some View {
        VStack {
          Text("Recent photos").font(.title2)
          ScrollView(.horizontal, showsIndicators: false) {
            HStack {
              ForEach(photos, id: \.self) { url in
                ImageView(withURL: url).frame(width: 200, height: 200).clipped()
              }
            }
          }
        }
      }
    }
    
    struct UsersView: View {
      private let users: [User]
      private let title: String
    
      init(title: String, users: [User]) {
        self.users = users
        self.title = title
      }
    
      var body: some View {
        VStack {
          Text(title).font(.title2)
          ScrollView(.horizontal, showsIndicators: false) {
            HStack {
              ForEach(users, id: \.self) { user in
                VStack {
                  ImageView(withURL: user.imageURL).frame(width: 80, height: 80).clipped()
                  Text(user.name)
                }
              }
            }
          }
        }
      }
    }
    
    struct ProfileHeaderView: View {
      private let user: User
      private let canSendMessage: Bool
      private let canStartVideoChat: Bool
    
      init(user: User, canSendMessage: Bool, canStartVideoChat: Bool) {
        self.user = user
        self.canSendMessage = canSendMessage
        self.canStartVideoChat = canStartVideoChat
      }
    
      var body: some View {
        VStack {
          HStack(alignment: .center, spacing: 16) {
            Spacer()
            if canStartVideoChat {
              Button(action: {}) {
                Image(systemName: "video")
              }
            }
            if canSendMessage {
              Button(action: {}) {
                Image(systemName: "message")
              }
            }
          }.padding(.trailing)
          ImageView(withURL: user.imageURL).clipShape(Circle()).frame(width: 100, height: 100).clipped()
          Text(user.name).font(.largeTitle)
          HStack {
            Image(systemName: "location")
            Text(user.area).font(.subheadline)
          }.padding(2)
          Text(user.bio).font(.body).padding()
        }
      }
    }
    
    // MARK: - History feed views
    
    struct HistoryFeedView: View {
      private let posts: [Post]
    
      init(posts: [Post]) {
        self.posts = posts
      }
    
      var body: some View {
        ScrollView(.vertical, showsIndicators: true) {
          VStack {
            Text("Recent posts").font(.title2)
            ForEach(posts, id: \.self) { post in
              PostView(post: post)
            }
          }
        }
      }
    }
    
    struct PostView: View {
      private let post: Post
    
      init(post: Post) {
        self.post = post
      }
    
      var body: some View {
        VStack {
          ImageView(withURL: post.pictureURL).frame(height: 200).clipped()
          HStack {
            Text(post.message)
            Spacer()
            HStack {
              Image(systemName: "hand.thumbsup")
              Text(String(post.likesCount))
            }
            HStack {
              Image(systemName: "bubble.right")
              Text(String(post.commentsCount))
            }
          }.padding()
        }
      }
    }
    
    struct RestrictedAccessView: View {
      var body: some View {
        VStack {
          Image(systemName: "eye.slash").padding()
          Text("The access to the full profile info is restricted")
        }
      }
    }
    
    5. ImageView.swift
    
    import SwiftUI
    
    struct ImageView: View {
      @ObservedObject var imageLoader: ImageLoader
      @State var image = UIImage(named: "img_placeholder", in: Bundle(for: ImageLoader.self), with: nil) ?? UIImage()
    
      init(withURL url: String) {
        self.imageLoader = ImageLoader(urlString: url)
      }
    
      var body: some View {
        Image(uiImage: image)
          .resizable()
          .aspectRatio(contentMode: .fill)
          .onReceive(imageLoader.didChange) { image in
            self.image = image
          }
      }
    }
    
    struct ImageView_Previews: PreviewProvider {
      static var previews: some View {
        Image(uiImage: UIImage(named: "img_placeholder") ?? UIImage())
          .resizable()
          .aspectRatio(contentMode: .fill)
          .frame(width: 100, height: 100)
          .clipped()
      }
    }
    
    6. SceneDelegate.swift
    
    import UIKit
    import SwiftUI
    
    class SceneDelegate: UIResponder, UIWindowSceneDelegate {
      var window: UIWindow?
    
      typealias Provider = ProfileContentProvider<PreferencesStore>
    
      func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        let container = DIContainer.shared
        container.register(type: PrivacyLevel.self, component: PrivacyLevel.friend)
        container.register(type: User.self, component: Mock.user())
        container.register(type: PreferencesStore.self, component: PreferencesStore())
        container.register(type: Provider.self, component: Provider())
        let profileView = ProfileView<Provider>()
        if let windowScene = scene as? UIWindowScene {
          let window = UIWindow(windowScene: windowScene)
          window.rootViewController = UIHostingController(rootView: profileView)
          self.window = window
          window.makeKeyAndVisible()
        }
      }
    }
    
    7. AppDelegate.swift
    
    import UIKit
    
    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate { }
    
    8. User.swift
    
    import Foundation
    
    struct User: Hashable, Equatable {
      let name: String
      let imageURL: String
      let bio: String
      let area: String
      let friends: [User]
      let photos: [String]
      let historyFeed: [Post]
    
      static func == (lhs: User, rhs: User) -> Bool {
        lhs.name == rhs.name && lhs.imageURL == rhs.imageURL
      }
    }
    
    struct Post: Hashable {
      let pictureURL: String
      let message: String
      let likesCount: Int
      let commentsCount: Int
    }
    
    9. Mock.swift
    
    import Foundation
    
    enum Mock {
      static func user() -> User {
        return User(
          name: "Belle",
          imageURL: "https://images.unsplash.com/photo-1522593596038-8a7b75a0f2bc?ixlib=rb-1.2.1&auto=format&fit=crop&w=2250&q=80",
          bio: "I love hiking, exploring new countries and traditional dishes",
          area: "Italy",
          friends: friends(),
          photos: photos(),
          historyFeed: posts()
        )
      }
    
      static func photos() -> [String] {
        return [
          "https://images.unsplash.com/photo-1553808354-74b53671c8b4?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80",
          "https://images.unsplash.com/photo-1570996915537-dd53ef642ef7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80",
          "https://images.unsplash.com/photo-1553808353-54724c294f0c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80",
          "https://images.unsplash.com/photo-1552396422-9d90144ceb97?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80",
          "https://images.unsplash.com/photo-1573314481772-97f0b6102c17?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80",
          "https://images.unsplash.com/photo-1586866928487-2af5b3850105?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80"
        ]
      }
    
      static func posts() -> [Post] {
        return [
          Post(pictureURL: "https://images.unsplash.com/photo-1440186347098-386b7459ad6b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80", message: "", likesCount: 20, commentsCount: 10),
          Post(pictureURL: "https://images.unsplash.com/photo-1501554728187-ce583db33af7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80", message: "Such a nice view!", likesCount: 32, commentsCount: 5),
          Post(pictureURL: "https://images.unsplash.com/photo-1500049242364-5f500807cdd7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80", message: "", likesCount: 20, commentsCount: 10),
          Post(pictureURL: "https://images.unsplash.com/photo-1442570468985-f63ed5de9086?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80", message: "", likesCount: 20, commentsCount: 10),
          Post(pictureURL: "https://images.unsplash.com/photo-1574325298943-eacd397f382d?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80", message: "I love train rides", likesCount: 20, commentsCount: 10),
          Post(pictureURL: "https://images.unsplash.com/photo-1549872178-96db16a53ca8?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80", message: "Ready for a new adventure", likesCount: 20, commentsCount: 10)
        ]
      }
    
      static func friends() -> [User] {
        return [
          User(name: "Leila", imageURL: "https://images.unsplash.com/photo-1474073705359-5da2a8270c64?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80", bio: "", area: "", friends: [], photos: [], historyFeed: []),
          User(name: "Ingrid", imageURL: "https://images.unsplash.com/photo-1520512202623-51c5c53957df?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80", bio: "", area: "", friends: [], photos: [], historyFeed: []),
          User(name: "Leon", imageURL: "https://images.unsplash.com/photo-1534614971-6be99a7a3ffd?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80", bio: "", area: "", friends: [], photos: [], historyFeed: []),
          User(name: "Jonathan", imageURL: "https://images.unsplash.com/photo-1484517186945-df8151a1a871?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80", bio: "", area: "", friends: [], photos: [], historyFeed: []),
          User(name: "Jay", imageURL: "https://images.unsplash.com/photo-1485361767769-5ceffc9f2144?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80", bio: "", area: "", friends: [], photos: [], historyFeed: []),
          User(name: "Harriette", imageURL: "https://images.unsplash.com/photo-1468817814611-b7edf94b5d60?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80", bio: "", area: "", friends: [], photos: [], historyFeed: [])
        ]
      }
    }
    
    10. ImageLoader.swift
    
    import Combine
    import SwiftUI
    
    class ImageLoader: ObservableObject {
      var didChange = PassthroughSubject<UIImage, Never>()
      var image = UIImage() {
        didSet {
          didChange.send(image)
        }
      }
    
      init(urlString: String) {
        guard let url = URL(string: urlString) else { return }
        let task = URLSession.shared.dataTask(with: url) { data, _, _ in
          guard let data = data else { return }
          DispatchQueue.main.async {
            self.image = UIImage(data: data) ?? UIImage()
          }
        }
        task.resume()
      }
    }
    
    11. PrivacySetting.swift
    
    enum PrivacySetting: String, CaseIterable {
      case photos = "Who can see my photos",
      friends = "Who can see my friends list",
      feed = "Who can see my feed",
      videoCall = "Who can video call me",
      message = "Who can message me"
    }
    
    12. ProfileContentProvider.swift
    
    import SwiftUI
    import Combine
    
    protocol ProfileContentProviderProtocol: ObservableObject {
      var privacyLevel: PrivacyLevel { get }
      var canSendMessage: Bool { get }
      var canStartVideoChat: Bool { get }
      var photosView: AnyView { get }
      var feedView: AnyView { get }
      var friendsView: AnyView { get }
    }
    
    final class ProfileContentProvider<Store>: ProfileContentProviderProtocol where Store: PreferencesStoreProtocol {
      let privacyLevel: PrivacyLevel
      private let user: User
      private var store: Store
      private var cancellables: Set<AnyCancellable> = []
    
      init(
        // swiftlint:disable force_unwrapping
        privacyLevel: PrivacyLevel = DIContainer.shared.resolve(type: PrivacyLevel.self)!,
        user: User = DIContainer.shared.resolve(type: User.self)!,
        store: Store = DIContainer.shared.resolve(type: Store.self)!
        // swiftlint:enable force_unwrapping
      ) {
        self.privacyLevel = privacyLevel
        self.user = user
        self.store = store
    
        store.objectWillChange.sink { _ in
          self.objectWillChange.send()
        }
        .store(in: &cancellables)
      }
    
      var canSendMessage: Bool {
        privacyLevel >= store.messagePreference
      }
    
      var canStartVideoChat: Bool {
        privacyLevel >= store.videoCallsPreference
      }
    
      var photosView: AnyView {
        privacyLevel >= store.photosPreference ? AnyView(PhotosView(photos: user.photos)) : AnyView(EmptyView())
      }
    
      var feedView: AnyView {
        privacyLevel >= store.feedPreference ? AnyView(HistoryFeedView(posts: user.historyFeed)) : AnyView(EmptyView())
      }
    
      var friendsView: AnyView {
        privacyLevel >= store.friendsListPreference ?
          AnyView(UsersView(title: "Friends", users: user.friends)) :
          AnyView(EmptyView())
      }
    }
    
    13. DIContainer.swift
    
    import Foundation
    
    protocol DIContainerProtocol {
      func register<Component>(type: Component.Type, component: Any)
      func resolve<Component>(type: Component.Type) -> Component?
    }
    
    final class DIContainer: DIContainerProtocol {
      static let shared = DIContainer()
    
      private init() {}
    
      var components: [String: Any] = [:]
    
      func register<Component>(type: Component.Type, component: Any) {
        components["\(type)"] = component
      }
    
      func resolve<Component>(type: Component.Type) -> Component? {
        return components["\(type)"] as? Component
      }
    }
    

    后记

    本篇主要讲述了基于SwiftUI简单App的Dependency Injection应用,感兴趣的给个赞或者关注~~~

    相关文章

      网友评论

          本文标题:SwiftUI框架详细解析 (十七) —— 基于SwiftUI简

          本文链接:https://www.haomeiwen.com/subject/nileoktx.html