Swift

3D Touch (二) —— Home Screen Quic

2022-04-06  本文已影响0人  刀客传奇

版本记录

版本号 时间
V1.0 2022.04.05 星期二 清明节

前言

3D TouchiPhone 6s+iOS9+之后新增的功能。其最大的好处在于不启动app的情况下,快速进入app中的指定界面,说白了,就是一个快捷入口。接下来几篇我们就一起看下相关的内容。感兴趣的可以看下面几篇文章。
1. 3D Touch (一) —— Home Screen Quick Actions for SwiftUI App(一)

源码

首先看下工程组织结构

下面看下源码了

1. AppMain.swift
import SwiftUI

// MARK: App Main
@main
struct AppMain: App {
  // MARK: Properties
  private let actionService = ActionService.shared
  private let noteStore = NoteStore.shared
  @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

  // MARK: Body
  var body: some Scene {
    WindowGroup {
      NoteList()
        .environmentObject(actionService)
        .environmentObject(noteStore)
        .environment(\.managedObjectContext, noteStore.container.viewContext)
    }
  }
}
2. AppDelegate.swift
// 1
import UIKit

// 2
class AppDelegate: NSObject, UIApplicationDelegate {
  private let actionService = ActionService.shared

  // 3
  func application(
    _ application: UIApplication,
    configurationForConnecting connectingSceneSession: UISceneSession,
    options: UIScene.ConnectionOptions
  ) -> UISceneConfiguration {
    // 4
    if let shortcutItem = options.shortcutItem {
      actionService.action = Action(shortcutItem: shortcutItem)
    }

    // 5
    let configuration = UISceneConfiguration(
      name: connectingSceneSession.configuration.name,
      sessionRole: connectingSceneSession.role
    )
    configuration.delegateClass = SceneDelegate.self
    return configuration
  }
}

// 6
class SceneDelegate: NSObject, UIWindowSceneDelegate {
  private let actionService = ActionService.shared

  // 7
  func windowScene(
    _ windowScene: UIWindowScene,
    performActionFor shortcutItem: UIApplicationShortcutItem,
    completionHandler: @escaping (Bool) -> Void
  ) {
    // 8
    actionService.action = Action(shortcutItem: shortcutItem)
    completionHandler(true)
  }
}
3. Note.swift
import Foundation
import CoreData
import UIKit

/// Extension of the generated `Note` Core Data model with added convenience.
extension Note {
  var wrappedTitle: String {
    get { title ?? "" }
    set { title = newValue }
  }

  var wrappedBody: String {
    get { body ?? "" }
    set { body = newValue }
  }

  var identifier: String {
    objectID.uriRepresentation().absoluteString
  }

  public override func willChangeValue(forKey key: String) {
    super.willChangeValue(forKey: key)

    // Helper to keep lastModified up-to-date when other properties are modified
    if key == "title" || key == "body" || key == "isFavorite" {
      lastModified = Date()
    }
  }

  // 1
  var shortcutItem: UIApplicationShortcutItem? {
    // 2
    guard !wrappedTitle.isEmpty || !wrappedBody.isEmpty else { return nil }

    // 3
    return UIApplicationShortcutItem(
      type: ActionType.editNote.rawValue,
      localizedTitle: "Edit Note",
      localizedSubtitle: wrappedTitle.isEmpty ? wrappedBody : wrappedTitle,
      icon: .init(systemImageName: isFavorite ? "star" : "pencil"),
      userInfo: [
        "NoteID": identifier as NSString
      ]
    )
  }
}
4. NoteStore.swift
import CoreData
import Foundation

class NoteStore: ObservableObject {
  /// A singleton instance for use within the app
  static let shared = NoteStore()

  /// A test configuration for SwiftUI previews
  static func preview() -> NoteStore {
    return NoteStore(inMemory: true)
  }

  /// Storage for Core Data
  let container = NSPersistentContainer(name: "NoteBuddy")

  private init(inMemory: Bool = false) {
    // Use an in-memory store if required (the default is persisted)
    if inMemory {
      let description = NSPersistentStoreDescription()
      description.type = NSInMemoryStoreType
      container.persistentStoreDescriptions = [description]
    }

    // Attempt to load the persistent stores
    container.loadPersistentStores { _, error in
      if let error = error {
        fatalError("Failed to load persistent store: \(error)")
      }
    }

    // If there is no content (i.e first use or it was deleted), populate with sample data
    do {
      if try container.viewContext.count(for: Note.fetchRequest()) == 0 {
        try createDefaultData()
      }
    } catch {
      fatalError("Failed to create initial sample data: \(error)")
    }
  }

  /// Helper for saving changes only when requred
  func saveIfNeeded() {
    guard container.viewContext.hasChanges else { return }
    try? container.viewContext.save()
  }

  /// Helper method for creating a new note
  func createNewNote() -> Note {
    let note = Note(context: container.viewContext)
    note.title = "My Note"
    note.body = "This is my new note."
    note.isFavorite = false
    note.lastModified = Date()

    try? container.viewContext.save()
    return note
  }

  /// Helper method for finding a `Note` with the given identifier
  func findNote(withIdentifier id: String) -> Note? {
    guard
      let uri = URL(string: id),
      let objectID = container.persistentStoreCoordinator.managedObjectID(forURIRepresentation: uri),
      let note = container.viewContext.object(with: objectID) as? Note
    else { return nil }
    return note
  }

  /// Private helper method for inserting demo note data into the main context
  private func createDefaultData() throws {
    let pumkinPie = Note(context: container.viewContext)
    pumkinPie.title = "Pumpkin Pie"
    pumkinPie.body = "For this recipe you will need some flour, butter, pumpkin, sugar, and spices."
    pumkinPie.isFavorite = true
    pumkinPie.lastModified = Date()

    let groceries = Note(context: container.viewContext)
    groceries.title = "Groceries"
    groceries.body = "1x Flour\n2x Bananas\n1x Tomato paste\n3x Oranges\n5x Onions"
    groceries.lastModified = Date()

    let newLaptop = Note(context: container.viewContext)
    newLaptop.title = ""
    newLaptop.body = [
      "With the new, M1-powered MacBook Pro laptops, I've been thinking about what ",
      "option is best for working with Xcode and doing some video editing. Perhaps ",
      "an M1 Pro Max with 64GB of RAM is the best option!"
    ].joined()
    newLaptop.isFavorite = true
    newLaptop.lastModified = Date()

    try container.viewContext.save()
  }
}
5. Action.swift
import UIKit

// 1
enum ActionType: String {
  case newNote = "NewNote"
  case editNote = "EditNote"
}

// 2
enum Action: Equatable {
  case newNote
  case editNote(identifier: String)

  // 3
  init?(shortcutItem: UIApplicationShortcutItem) {
    // 4
    guard let type = ActionType(rawValue: shortcutItem.type) else {
      return nil
    }

    // 5
    switch type {
    case .newNote:
      self = .newNote
    case .editNote:
      if let identifier = shortcutItem.userInfo?["NoteID"] as? String {
        self = .editNote(identifier: identifier)
      } else {
        return nil
      }
    }
  }
}

// 6
class ActionService: ObservableObject {
  static let shared = ActionService()

  // 7
  @Published var action: Action?
}
6. EditNote.swift
import SwiftUI

// MARK: Edit Note
struct EditNote: View {
  // MARK: Editor
  private enum Editor: Hashable {
    case title
    case body
  }

  // MARK: Properties

  /// The text input that is currently focused
  @FocusState private var focusedEditor: Editor?

  /// The note being edited
  @ObservedObject var note: Note

  /// The store of all notes used for saving changes
  @EnvironmentObject var noteStore: NoteStore

  // MARK: Body
  var body: some View {
    VStack(alignment: .leading) {
      HStack {
        TextField("Title", text: $note.wrappedTitle)
          .padding(.horizontal, 6)
          .font(.system(size: 18, weight: .bold, design: .default))
          .frame(minHeight: 35, maxHeight: 35)
          .border(Color.accentColor, width: focusedEditor == .title || note.wrappedTitle.isEmpty ? 1 : 0)
          .focused($focusedEditor, equals: .title)
        Button(action: toggleFavorite) {
          Image(systemName: note.isFavorite ? "star.fill" : "star")
            .foregroundColor(note.isFavorite ? .yellow : .gray)
        }
      }
      TextEditor(text: $note.wrappedBody)
        .font(.system(size: 14, weight: .regular, design: .default))
        .border(Color.accentColor, width: focusedEditor == .body || note.wrappedBody.isEmpty ? 1 : 0)
        .focused($focusedEditor, equals: .body)
    }
    .navigationBarTitleDisplayMode(.inline)
    .navigationTitle("Edit Note")
    .padding(8.0)
    .onDisappear(perform: onDisappear)
  }

  // MARK: Actions

  func toggleFavorite() {
    note.isFavorite.toggle()
  }

  func onDisappear() {
    noteStore.saveIfNeeded()
  }
}

// MARK: Previews
struct EditNote_Previews: PreviewProvider {
  static let noteStore = NoteStore.preview()

  static var notes: [Note] {
    (try? noteStore.container.viewContext.fetch(Note.fetchRequest())) ?? []
  }

  static var previews: some View {
    EditNote(note: notes[0])
      .previewLayout(.sizeThatFits)
      .environmentObject(noteStore)
  }
}
7. NoteList.swift
import CoreData
import SwiftUI

// MARK: Note List
struct NoteList: View {
  // MARK: Properties

  /// The Core Data query used to populate the notes presented within the list
  @FetchRequest(
    entity: Note.entity(),
    sortDescriptors: [
      NSSortDescriptor(keyPath: \Note.lastModified, ascending: false)
    ]
  ) var notes: FetchedResults<Note>

  /// The currently selected note (binded to the navigation of the `EditNote` view)
  @State private var selectedNote: Note?

  /// The class used for managing the stored notes
  @EnvironmentObject var noteStore: NoteStore
  @EnvironmentObject var actionService: ActionService
  @Environment(\.scenePhase) var scenePhase

  // MARK: Body
  var body: some View {
    NavigationView {
      ZStack {
        // MARK: Navigation
        NavigationLink(value: $selectedNote) { note in
          EditNote(note: note)
        }

        // MARK: Content
        List {
          ForEach(notes) { note in
            Button(
              action: { selectedNote = note },
              label: { NoteRow(note: note) }
            )
          }
          .onDelete(perform: deleteNotes(atIndexSet:))
        }
        .listStyle(InsetGroupedListStyle())
        .navigationTitle("Notes")
        .toolbar {
          ToolbarItem(placement: .navigationBarTrailing) {
            Button(action: createNewNote) {
              Image(systemName: "square.and.pencil")
            }
          }
        }
        // 1
        .onChange(of: scenePhase) { newValue in
          // 2
          switch newValue {
          case .active:
            performActionIfNeeded()
          case .background:
            updateShortcutItems()
          // 3
          default:
            break
          }
        }
      }
    }
  }

  // MARK: Actions

  /// Deletes each note and commits the change
  func deleteNotes(atIndexSet indexSet: IndexSet) {
    for index in indexSet {
      noteStore.container.viewContext.delete(notes[index])
    }
    noteStore.saveIfNeeded()
  }

  /// Creates a new note and selects it immediately
  func createNewNote() {
    selectedNote = noteStore.createNewNote()
  }

  func performActionIfNeeded() {
    // 1
    guard let action = actionService.action else { return }

    // 2
    switch action {
    case .newNote:
      createNewNote()
    case .editNote(let identifier):
      selectedNote = noteStore.findNote(withIdentifier: identifier)
    }

    // 3
    actionService.action = nil
  }

  func updateShortcutItems() {
    UIApplication.shared.shortcutItems = notes.compactMap(\.shortcutItem)
  }
}

// MARK: Previews
struct NoteList_Previews: PreviewProvider {
  static let noteStore = NoteStore.preview()

  static var previews: some View {
    NoteList()
      .environmentObject(noteStore)
      .environment(\.managedObjectContext, noteStore.container.viewContext)
  }
}
8. NoteRow.swift
import SwiftUI

// MARK: Note Row
struct NoteRow: View {
  // MARK: Properties
  @ObservedObject var note: Note

  // MARK: Body
  var body: some View {
    HStack {
      VStack(alignment: .leading, spacing: 2.0) {
        if !note.wrappedTitle.isEmpty {
          Text(note.wrappedTitle)
            .font(.system(size: 15, weight: .bold, design: .default))
            .multilineTextAlignment(.leading)
            .lineLimit(1)
        }
        Text(note.wrappedBody)
          .font(.system(size: 14, weight: .regular, design: .default))
          .multilineTextAlignment(.leading)
          .lineLimit(3)
      }
      Spacer()
      Label("Toggle Favorite", systemImage: note.isFavorite ? "star.fill" : "star")
        .labelStyle(.iconOnly)
        .foregroundColor(note.isFavorite ? .yellow : .gray)
    }
    .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 8))
    .foregroundColor(Color(.label))
  }
}

// MARK: Previews
struct NotesRow_Previews: PreviewProvider {
  static let noteStore = NoteStore.preview()

  static var notes: [Note] {
    (try? noteStore.container.viewContext.fetch(Note.fetchRequest())) ?? []
  }

  static var previews: some View {
    NoteRow(note: notes[2])
      .previewLayout(.fixed(width: 300, height: 70))
  }
}
9. NavigationLink+Value.swift


extension NavigationLink where Label == EmptyView {
  /// Convenience initializer used to bind a `NavigationLink` to an optional value.
  ///
  /// When a non-nil value is assigned, the link will be triggered. Setting the value to `nil` will pop the navigation link again.
  ///
  /// ```swift
  /// struct MyListView: View {
  ///   @EnvironmentObject var modelStore: ModelStore
  ///   @State private var selectedModel: MyModel?
  ///
  ///   var body: someView {
  ///     NavigationView {
  ///       ZStack {
  ///         // Navigation
  ///         NavigationLink(value: $selectedModel) { model in
  ///           MyDetailView(model: model)
  ///         }
  ///
  ///         // List Content
  ///         ForEach(modelStore) { model in
  ///           Button(action: { selectedModel = model }) {
  ///             MyRow(model: model)
  ///           }
  ///         }
  ///       }
  ///     }
  ///   }
  /// }
  /// ```
  init<Value, D: View>(
    value: Binding<Value?>,
    @ViewBuilder destination: @escaping (Value) -> D
  ) where Destination == D? {
    // Create wrapping arguments that erase the value from the destination and binding
    let destination = value.wrappedValue.map { destination($0) }
    let isActive = Binding<Bool>(
      get: { value.wrappedValue != nil },
      set: { newValue in
        if newValue {
          assertionFailure("Programatically setting isActive to `true` is not supported and will be ignored")
        }
        value.wrappedValue = nil
      }
    )

    // Invoke the original initializer with the mapped destination and correct binding
    self.init(destination: destination, isActive: isActive) {
      EmptyView()
    }
  }
}

后记

本篇主要讲述了Home Screen Quick Actions for SwiftUI App,感兴趣的给个赞或者关注~~~

上一篇下一篇

猜你喜欢

热点阅读