BetterSheet 1.0.0

BetterSheet 1.0.0

Peter Verhage 维护。



  • 作者:
  • Peter Verhage

BetterSheet

提供具有以下功能的强大 SwiftUI 抽屉扩展:

  • 内置 sheet 修饰符的所有功能,但更健壮(针对 Xcode 11.0 公测版 5 进行了测试)。
  • 支持模态(防止用户滑动关闭),类似于 UIKit 的 modalInPresentation
  • 当用户尝试模态时关闭抽屉时,支持调用操作。

希望苹果会在 iOS 13.0 最终版本发布之前,增加默认 sheet 修饰符的健壮性并将模态展示支持加入,这样该库就变得过时了。

基本使用

首先确保您在 SceneDelegate.swift 中导入 BetterSheet 包并将 UIHostingController 初始化为具有高能抽屉支持

window.rootViewController = UIHostingController.withBetterSheetSupport(rootView: ContentView())

显示抽屉的基本 API 类似于 SwiftUI 的 sheet(isPresented:onDismiss:content:) 视图修饰符。但您不是使用 sheet 而是用 betterSheet

例如

struct ContentView: View {
    @State var showDetail = false

    var body: some View {
        VStack {
            Button(action: { self.showDetail = true }) {
                Text("Show Detail")
            }
        }
            .betterSheet(isPresented: $showDetail) {
                Text("Detail!")
            }
    }
}

对于更高级的使用案例,有一个类似于 SwiftUI 的 sheet(item:onDismiss:content: 视图修饰符的 API 可用

struct Fruit {
    let name: String
}

extension Fruit: Identifiable {
    var id: String {
        name
    }
}

struct ContentView: View {
    let fruits = [Fruit(name: "Apple"), Fruit(name: "Banana"), Fruit(name: "Orange")]
    @State var selectedFruit: Fruit? = nil

    var body: some View {
        List(fruits) { fruit in
            Button(action: { self.selectedFruit = fruit }) {
                Text(fruit.name)
            }
        }
            .betterSheet(item: $selectedFruit) { fruit in
                Text("You selected \(fruit.name)")
            }
    }
}

正如 SwiftUI 的 sheet 修饰符一样,还有一个环境值类似于 SwiftUI 的 presentationMode 可用,您可以使用它从自己的代码中关闭抽屉。BetterSheet 的这个环境值的版本称为 betterSheetPresentationMode.

一个示例

struct DetailView: View {
    @Environment(\.betterSheetPresentationMode) var presentationMode
    
    var body: some View {
        Button(action: { self.presentationMode.value.dismiss() }) {
            Text("Dismiss")
        }
    }    
}

struct ContentView: View {
    @State var showDetail = false

    var body: some View {
        VStack {
            Button(action: { self.showDetail = true }) {
                Text("Show Detail")
            }
        }
            .betterSheet(isPresented: $showDetail) {
                DetailView()
            }
    }
}

高级使用

到目前为止,我们只看了那些提供与默认SwiftUI视图控制器类似功能的API。但是,BetterSheet提供了一些更高级的功能,如果你不希望用户通过轻扫手势简单地关闭你的视图控制器。

例如

struct Fruit {
    let name: String
}

extension Fruit: Identifiable {
    var id: String {
        name
    }
}

struct EditView: View {
    @Binding var fruits: [Fruit]
    
    let fruit: Fruit?
    @State var name: String
    
    @Environment(\.betterSheetPresentationMode) var presentationMode
    
    @State var showDismissActions = false
    
    init(fruits: Binding<[Fruit]>, fruit: Fruit? = nil) {
        _fruits = fruits
        self.fruit = fruit
        _name = State(initialValue: fruit?.name ?? "")
    }
    
    var isNew: Bool {
        fruit == nil
    }
    
    var isValid: Bool {
        name.trimmingCharacters(in: .whitespaces).count > 0
    }
    
    var isModified: Bool {
        if let fruit = fruit, name != fruit.name {
            return true
        } else if fruit == nil && isValid {
            return true
        } else {
            return false
        }
    }
    
    var body: some View {
        NavigationView {
            Form {
                HStack {
                    Text("Name")
                    TextField("Fruit", text: $name).multilineTextAlignment(.trailing)
                }
            }
                .navigationBarTitle(fruit == nil ? "Add Fruit" : "Edit Fruit")
                .navigationBarItems(
                    leading: Button(action: save) { Text("Save").fontWeight(.bold).disabled(!isValid) },
                    trailing: Button(action: self.cancel) { Text("Cancel") }
                )
                .actionSheet(isPresented: $showDismissActions) {
                    ActionSheet(
                        title: Text("Select an option"),
                        message: nil,
                        buttons: [
                            .destructive(Text(isNew ? "Discard Fruit" : "Discard Changes"), action: self.cancel),
                            .default(Text(isNew ? "Add Fruit" : "Save Fruit"), action: self.save),
                            .cancel()
                        ]
                    )
                }
                .betterSheetIsModalInPresentation(isModified)
                .onBetterSheetDidAttemptToDismiss {
                    self.showDismissActions = true
                }
        }
    }

    func save() {
        guard isValid else { return }
        
        let fruit = Fruit(name: name)
        
        if let index = fruits.firstIndex(where: { $0.id == self.fruit?.id }) {
            fruits.remove(at: index)
            fruits.insert(fruit, at: index)
        } else {
            fruits.append(fruit)
        }
        
        presentationMode.value.dismiss()
    }
    
    func cancel() {
        presentationMode.value.dismiss()
    }
}

struct ContentView: View {
    @State var fruits: [Fruit] = [Fruit(name: "Apple")]

    @State var addFruit = false
    @State var editFruit: Fruit? = nil

    var body: some View {
        NavigationView {
            List(fruits) { fruit in
                Text(fruit.name)
                Spacer()
                Button(action: { self.editFruit = fruit }) {
                    Image(systemName: "pencil.circle")
                }
            }
                .listStyle(GroupedListStyle())
                .navigationBarTitle("Fruits")
                .navigationBarItems(
                    leading: Button(action: { self.addFruit = true }) { Text("Add") }
                )
                .betterSheet(isPresented: $addFruit) {
                    EditView(fruits: self.$fruits)
                }
                .betterSheet(item: $editFruit) { fruit in
                    EditView(fruits: self.$fruits, fruit: fruit)
                }
        }
    }
}

许可

本项目根据MIT лицензии进行许可。请参阅LICENSE文件。