I'm using this maps drawer library which is written in UIKit in a SwiftUI project. I have a SwiftUI ListView that I'm using in the project via a UIHostingController but I want to disable scrolling when the drawers position is not open but I'm not sure how to pass drawer position data to the ListView in order to disable it.
#objc public class PulleyPosition: NSObject {
public static let collapsed = PulleyPosition(rawValue: 0)
public static let partiallyRevealed = PulleyPosition(rawValue: 1)
public static let open = PulleyPosition(rawValue: 2)
public static let closed = PulleyPosition(rawValue: 3)
public static let all: [PulleyPosition] = [
.collapsed,
.partiallyRevealed,
.open,
.closed
]
public static let compact: [PulleyPosition] = [
.collapsed,
.open,
.closed
]
public let rawValue: Int
public init(rawValue: Int) {
if rawValue < 0 || rawValue > 3 {
print("PulleyViewController: A raw value of \(rawValue) is not supported. You have to use one of the predefined values in PulleyPosition. Defaulting to `collapsed`.")
self.rawValue = 0
} else {
self.rawValue = rawValue
}
}
/// Return one of the defined positions for the given string.
///
/// - Parameter string: The string, preferably obtained by `stringFor(position:)`
/// - Returns: The `PulleyPosition` or `.collapsed` if the string didn't match.
public static func positionFor(string: String?) -> PulleyPosition {
guard let positionString = string?.lowercased() else {
return .collapsed
}
switch positionString {
case "collapsed":
return .collapsed
case "partiallyrevealed":
return .partiallyRevealed
case "open":
return .open
case "closed":
return .closed
default:
print("PulleyViewController: Position for string '\(positionString)' not found. Available values are: collapsed, partiallyRevealed, open, and closed. Defaulting to collapsed.")
return .collapsed
}
}
public override func isEqual(_ object: Any?) -> Bool {
guard let position = object as? PulleyPosition else {
return false
}
return self.rawValue == position.rawValue
}
public override var description: String {
switch rawValue {
case 0:
return "collapsed"
case 1:
return "partiallyrevealed"
case 2:
return "open"
case 3:
return "closed"
default:
return "collapsed"
}
}
}
inside PulleyViewController: UIViewController
(maybe pass the drawerPosition through a UIViewRepresentable Coordinator?)
public fileprivate(set) var drawerPosition: PulleyPosition = .collapsed {
didSet {
setNeedsStatusBarAppearanceUpdate()
}
}
SwiftUI ListView
import SwiftUI
//class ListViewDelegate: ObservableObject {
// var pulleyPosition = PulleyPosition
//
struct ListView: View {
#EnvironmentObject var drawerPositionModel: DrawerPositionVM
var body: some View {
ScrollViewReader { proxy in
VStack {
Button("Jump to #50") {
proxy.scrollTo(50)
}
List(0..<100, id: \.self) { i in
Text("Example \(i)")
.id(i)
}.scrollDisabled(drawerPositionModel == .partiallyRevealed ? true : false)
}
}
}
}
class ListViewVHC: UIHostingController<ListView> {
required init?(coder: NSCoder) {
super.init (coder: coder, rootView: ListView())
}
}
struct ListView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
Related
I'm building a custom component and using ForEach with a custom generic type. The issue is that this type is throwing this error:
Cannot convert value of type '[ASValue]' to expected argument type 'Binding'
public struct ArrayStepperSection<T: Hashable>: Hashable {
public let header: String
public var items: [ASValue<T>]
public init(header: String = "", items: [ASValue<T>]) {
self.header = header
self.items = items
}
}
public struct ASValue<T: Hashable>: Hashable {
private let id = UUID()
var item: T
public init(item: T) {
self.item = item
}
}
public class ArrayStepperValues<T: Hashable>: Hashable, ObservableObject {
#Published public var values: [ASValue<T>]
#Published public var selected: ASValue<T>
#Published public var sections: [ArrayStepperSection<T>]
public init(values: [ASValue<T>], selected: ASValue<T>, sections: [ArrayStepperSection<T>]? = nil) {
self.values = values
self.selected = selected
if sections != nil {
self.sections = sections!
} else {
self.sections = [ArrayStepperSection(items: values)]
}
}
public static func == (lhs: ArrayStepperValues<T>, rhs: ArrayStepperValues<T>) -> Bool {
return lhs.sections == rhs.sections
}
public func hash(into hasher: inout Hasher) {
hasher.combine(sections)
}
}
struct ArrayStepperList<T: Hashable>: View {
#Environment(\.dismiss) var dismiss
#ObservedObject var values: ArrayStepperValues<T>
let display: (T) -> String
var body: some View {
List {
ForEach(values.sections, id: \.self) { section in
Section(section.header) {
ForEach(section.items, id: \.self) { item in // Error happens here
Button(action: {
values.selected.item = item
dismiss()
}) {
HStack {
Text(display(item))
Spacer()
if values.selected.item == item {
Image(systemName: "checkmark")
}
}
}
}
}
}
}
.listStyle(InsetGroupedListStyle())
.navigationTitle(Text(display(values.selected.item)))
}
}
here is the test code I used to remove the error:
struct ContentView: View {
#StateObject var values = ArrayStepperValues(values: [ASValue(item: "aaa"),ASValue(item: "bbb")], selected: ASValue(item: "aaa"))
var body: some View {
ArrayStepperList(values: values) // for testing
}
}
struct ArrayStepperList<T: Hashable>: View {
#Environment(\.dismiss) var dismiss
#ObservedObject var values: ArrayStepperValues<T>
// let display: (T) -> String // for testing
var body: some View {
List {
ForEach(values.sections, id: \.self) { section in
Section(section.header) {
ForEach(section.items, id: \.self) { item in
Button(action: {
DispatchQueue.main.async {
values.selected.item = item.item // <-- here
// dismiss() // for testing
}
}) {
HStack {
Text("\(item.item as! String)") // for testing
Spacer()
if values.selected.item == item.item { // <-- here
Image(systemName: "checkmark")
}
}
}
}
}
}
}
.listStyle(InsetGroupedListStyle())
// .navigationTitle(Text(display(values.selected.item))) // for testing
}
}
I have an example struct:
public struct Axis: Hashable, CustomStringConvertible {
public var name: String
public var description: String {
return "Axis: \"\(name)\""
}
}
And property wrapper to make some operations on [Axis] struct.
#propertyWrapper
struct WrappedAxes {
var wrappedValue: [Axis] {
// This is just example, in real world it's much more complicated.
didSet {
for index in wrappedValue.indices {
var elems = Array(wrappedValue[index].name.split(separator: " "))
if elems.count>1 {
elems.removeLast()
}
let new = elems.reduce(into:"", {$0 += "\($1) "})
wrappedValue[index].name = new+("\(Date())")
} } } }
And I try to add, insert and remove Axes in SwiftUI View:
public struct ContentView: View {
#Binding var axes: [Axis]
public var body: some View {
VStack {
ForEach(axes.indices, id:\.self) {index in
HStack {
TextField("", text: $axes[index].name)
Button("Delete", action: {deleteAxis(index)})
Button("Insert", action: {insertAxis(index)})
}
}
Button("Add", action: addAxis)
}
}
var addAxis: () -> Void {
return {
axes.append(Axis(name: "New"))
print (axes)
}
}
var deleteAxis: (_:Int)->Void {
return {
if $0 < axes.count {
axes.remove(at: $0)
}
print (axes)
}
}
var insertAxis: (_:Int)->Void {
return {
if $0 < axes.count {
axes.insert(Axis(name: "Inserted"), at: $0)
}
print (axes)
}
}
public init (axes: Binding<[Axis]>) {
self._axes = axes
}
}
As far, as print (axes) shows changes are made, View never updates. I made very small App to test in which I call ContentView:
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
#WrappedAxes var axes = [Axis(name: "FirstOne")]
func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentView = ContentView(
axes: Binding (
get: {self.axes},
set: { [self] in axes = $0}))
.... // No fancy stuff
I'm open for all critique of code itself, and help: how to push this view (and all possible future subviews) to update when axes changed?
The thing is that #Binding is for nested Views. When you want to make changes in a SwiftUI view, you have to use the #State instead in the one where the action starts. Plus, you don't need to set an initialiser this way. You can set your value like this, inside an ObservableObject to handle your logic:
struct Axis {
var name: String
var description: String {
return "Axis: \"\(name)\""
}
}
final class AxisViewModel: ObservableObject {
#Published var axes: [Axis] = [Axis(name: "First")]
init() { handleAxes() }
func addAxis() {
axes.append(Axis(name: "New"))
handleAxes()
}
func insertAxis(at index: Int) {
axes.insert(Axis(name: "Inserted"), at: index)
handleAxes()
}
func handleAxes() {
for index in axes.indices {
var elems = Array(axes[index].name.split(separator: " "))
if elems.count > 1 {
elems.removeLast()
}
let new = elems.reduce(into:"", { $0 += "\($1) " })
axes[index].name = new + ("\(Date())")
}
}
}
struct ContentView: View {
#ObservedObject var viewModel = AxisViewModel()
var body: some View {
VStack {
ForEach(viewModel.axes.indices, id:\.self) { index in
HStack {
TextField("", text: $viewModel.axes[index].name)
Button("Insert", action: { viewModel.insertAxis(at: index) })
}
}
Button("Add", action: viewModel.addAxis)
}
}
}
I made a new struct Axes:
public struct Axes {
#WrappedAxes var _axes: [Axis]
public subscript (index: Int) -> Axis {
get { return _axes[index]}
set { _axes[index] = newValue}
}
// and all needed functions and vars to simulate Array behaviour:
public mutating func append(_ newElement: Axis ) {
_axes.append(newElement)
}
public mutating func insert(_ newElement: Axis, at index: Int ) {
_axes.insert(newElement, at: index)
}
public mutating func remove(at index: Int ) {
_axes.remove(at: index)
}
....
}
and put it into #ObservalbeObject:
public class Globals: ObservableObject {
#Published public var axes: Axes
....
}
Then defined AxesView as
public struct AxesView: View {
#ObservedObject var globals: Globals
public var body: some View {
...
}
...
}
That's all. For a while it works.
Below is some code I have been trying
import SwiftUI
struct AnyOptional: View {
private var optionalArray: [Any?] = [1, 2, 3]
var body: some View {
VStack {
ForEach(optionalArray) { i in
Text("\(i)")
}
}
}
}
extension Optional: Identifiable {
public var id: String { self as! String }
}
struct AnyOptional_Previews: PreviewProvider {
static var previews: some View {
AnyOptional()
}
}
I had a similar problem with [String] which I solved by using this extension
extension String: Identifiable {
public var id: String { self }
}
but now I get an error saying Any? must inherit from NSObject.
Is there an easier way to do this?
A possible solution is to use your already created id extension:
var body: some View {
VStack {
ForEach(optionalArray) { i in
Text(i.id)
}
}
}
Note that not all objects can be casted down to String (self as! String will fail if the object can't be cast to String).
A better way is to use String(describing:).
For this you can create another extension (updated to remove the word Optional if there's some value):
extension Optional {
public var asString: String {
if let value = self {
return .init(describing: value)
}
return .init(describing: self)
}
}
and use it in the ForEach loop:
var body: some View {
VStack {
ForEach(optionalArray, id: \.asString) { i in
Text(i.asString)
}
}
}
I've got a very simple app example that has two views: a MasterView and a DetailView. The MasterView is presented inside a ContentView with a NavigationView:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
MasterView(viewModel: MasterViewModel())
.navigationBarTitle(Text("Master"))
.navigationBarItems(
leading: EditButton()
)
}
}
}
struct MasterView: View {
#ObservedObject private var viewModel: MasterViewModel
init(viewModel: MasterViewModel) {
self.viewModel = viewModel
}
var body: some View {
print("Test")
return DataStatusView(dataSource: self.$viewModel.result) { texts -> AnyView in
print("Closure")
return AnyView(List {
ForEach(texts, id: \.self) { text in
NavigationLink(
destination: DetailView(viewModel: DetailViewModel(stringToDisplay: text))
) {
Text(text)
}
}
})
}.onAppear {
if case .waiting = self.viewModel.result {
self.viewModel.fetch()
}
}
}
}
struct DetailView: View {
#ObservedObject private var viewModel: DetailViewModel
init(viewModel: DetailViewModel) {
self.viewModel = viewModel
}
var body: some View {
self.showView().onAppear {
self.viewModel.fetch()
}
.navigationBarTitle(Text("Detail"))
}
func showView() -> some View {
switch self.viewModel.result {
case .found(let s):
return AnyView(Text(s))
default:
return AnyView(Color.red)
}
}
}
The DataStatusView is a simple view to manage some state:
public enum ResultState<T, E: Error> {
case waiting
case loading
case found(T)
case failed(E)
}
struct DataStatusView<Content, T>: View where Content: View {
#Binding private(set) var dataSource: ResultState<T, Error>
private let content: (T) -> Content
private let waitingContent: AnyView?
#inlinable init(dataSource: Binding<ResultState<T, Error>>,
waitingContent: AnyView? = nil,
#ViewBuilder content: #escaping (T) -> Content) {
self._dataSource = dataSource
self.waitingContent = waitingContent
self.content = content
}
var body: some View {
self.buildMainView()
}
private func buildMainView() -> some View {
switch self.dataSource {
case .waiting:
return AnyView(Color.red)
case .loading:
return AnyView(Color.green)
case .found(let data):
return AnyView(self.content(data))
case .failed:
return AnyView(Color.yellow)
}
}
}
and the view models are a very simple "pretend to make a network call" vm:
final class MasterViewModel: ObservableObject {
#Published var result: ResultState<[String], Error> = .waiting
init() { }
func fetch() {
self.result = .loading
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
guard let self = self else { return }
self.result = .found(["This", "is", "a", "test"])
}
}
}
final class DetailViewModel: ObservableObject {
#Published var result: ResultState<String, Error> = .waiting
private let stringToDisplay: String
init(stringToDisplay: String) {
self.stringToDisplay = stringToDisplay
}
func fetch() {
self.result = .loading
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
guard let self = self else { return }
self.result = .found(self.stringToDisplay)
}
}
}
Now the problem I'm having is that every time I go from Master -> Detail view the block inside the DataStatusView is called. This is a problem because the "DetailView" is constantly re-created (and therefore its vm too, which causes the loading of the detail's data to fail).
This is happening because when I go from master -> detail the buttons in the navigation bar change (or at least that's the hypothesis). When I remove the lines:
.navigationBarItems(
leading: EditButton()
)
This works as "expected".
What is the "SwiftUI" way of dealing with this? A sample project that shows this issue is here: https://github.com/kerrmarin/swiftui-mvvm-master-detail
From within a property wrapper in Swift, can you someone refer back to the instance of the class or struck that owns the property being wrapped? Using self doesn't obviously work, nor does super.
I tried to pass in self to the property wrapper's init() but that doesn't work either because self on Configuration is not yet defined when #propertywrapper is evaluated.
My use case is in a class for managing a large number of settings or configurations. If any property is changed, I just want to notify interested parties that something changed. They don't really need to know which value just, so use something like KVO or a Publisher for each property isn't really necessary.
A property wrapper looks ideal, but I can't figure out how to pass in some sort of reference to the owning instance that the wrapper can call back to.
References:
SE-0258
enum PropertyIdentifier {
case backgroundColor
case textColor
}
#propertyWrapper
struct Recorded<T> {
let identifier:PropertyIdentifier
var _value: T
init(_ identifier:PropertyIdentifier, defaultValue: T) {
self.identifier = identifier
self._value = defaultValue
}
var value: T {
get { _value }
set {
_value = newValue
// How to callback to Configuration.propertyWasSet()?
//
// [self/super/...].propertyWasSet(identifier)
}
}
}
struct Configuration {
#Recorded(.backgroundColor, defaultValue:NSColor.white)
var backgroundColor:NSColor
#Recorded(.textColor, defaultValue:NSColor.black)
var textColor:NSColor
func propertyWasSet(_ identifier:PropertyIdentifier) {
// Do something...
}
}
The answer is no, it's not possible with the current specification.
I wanted to do something similar. The best I could come up with was to use reflection in a function at the end of init(...). At least this way you can annotate your types and only add a single function call in init().
fileprivate protocol BindableObjectPropertySettable {
var didSet: () -> Void { get set }
}
#propertyDelegate
class BindableObjectProperty<T>: BindableObjectPropertySettable {
var value: T {
didSet {
self.didSet()
}
}
var didSet: () -> Void = { }
init(initialValue: T) {
self.value = initialValue
}
}
extension BindableObject {
// Call this at the end of init() after calling super
func bindProperties(_ didSet: #escaping () -> Void) {
let mirror = Mirror(reflecting: self)
for child in mirror.children {
if var child = child.value as? BindableObjectPropertySettable {
child.didSet = didSet
}
}
}
}
You cannot do this out of the box currently.
However, the proposal you refer to discusses this as a future direction in the latest version:
https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#referencing-the-enclosing-self-in-a-wrapper-type
For now, you would be able to use a projectedValue to assign self to.
You could then use that to trigger some action after setting the wrappedValue.
As an example:
import Foundation
#propertyWrapper
class Wrapper {
let name : String
var value = 0
weak var owner : Owner?
init(_ name: String) {
self.name = name
}
var wrappedValue : Int {
get { value }
set {
value = 0
owner?.wrapperDidSet(name: name)
}
}
var projectedValue : Wrapper {
self
}
}
class Owner {
#Wrapper("a") var a : Int
#Wrapper("b") var b : Int
init() {
$a.owner = self
$b.owner = self
}
func wrapperDidSet(name: String) {
print("WrapperDidSet(\(name))")
}
}
var owner = Owner()
owner.a = 4 // Prints: WrapperDidSet(a)
My experiments based on : https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#referencing-the-enclosing-self-in-a-wrapper-type
protocol Observer: AnyObject {
func observableValueDidChange<T>(newValue: T)
}
#propertyWrapper
public struct Observable<T: Equatable> {
public var stored: T
weak var observer: Observer?
init(wrappedValue: T, observer: Observer?) {
self.stored = wrappedValue
}
public var wrappedValue: T {
get { return stored }
set {
if newValue != stored {
observer?.observableValueDidChange(newValue: newValue)
}
stored = newValue
}
}
}
class testClass: Observer {
#Observable(observer: nil) var some: Int = 2
func observableValueDidChange<T>(newValue: T) {
print("lol")
}
init(){
_some.observer = self
}
}
let a = testClass()
a.some = 4
a.some = 6
The answer is yes! See this answer
Example code for calling ObservableObject publisher with a UserDefaults wrapper:
import Combine
import Foundation
class LocalSettings: ObservableObject {
static var shared = LocalSettings()
#Setting(key: "TabSelection")
var tabSelection: Int = 0
}
#propertyWrapper
struct Setting<T> {
private let key: String
private let defaultValue: T
init(wrappedValue value: T, key: String) {
self.key = key
self.defaultValue = value
}
var wrappedValue: T {
get {
UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
public static subscript<EnclosingSelf: ObservableObject>(
_enclosingInstance object: EnclosingSelf,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, T>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Setting<T>>
) -> T {
get {
return object[keyPath: storageKeyPath].wrappedValue
}
set {
(object.objectWillChange as? ObservableObjectPublisher)?.send()
UserDefaults.standard.set(newValue, forKey: object[keyPath: storageKeyPath].key)
}
}
}