I have a Core Data object called Config on ContentView.
This object is created like this:
#ObservedObject private var config = Config.with(PersistenceController.shared.context)!
config has 3 boolean properties: turnedOn, isSoft, and willAppear.
turnedON and willAppear are created in a way that if turnedON is true, willAppear must be false and vice-versa.
Now I pass config to 3 toggle switches and let the user adjust each one, true or false, but remember, if the user turns turnedON true, willAppear must be false and vice-versa. The same is true if the user turns willAppear true, in that canse turnedON must switch to false and vice-versa too.
So I pass config to the toggle switches like this on ContentView:
CustomToggle("Turn it on?"),
config,
coreDataBooleanPropertyName:"turnedOn")
CustomToggle("Is it soft?"),
config,
coreDataBooleanPropertyName:"isSoft")
CustomToggle("Will it appear?"),
config,
coreDataBooleanPropertyName:"willAppear")
and this is CustomToggle...
import SwiftUI
import CoreData
struct CustomToggle: View {
#State private var status:Bool
private let title: String
private let coreDataBooleanPropertyName:String
#ObservedObject private var config:Config {
didSet {
switch coreDataBooleanPropertyName {
case "isSoft":
self.status = config.isSoft
case "turnedOn":
self.status = config.turnedOn
case "willAppear":
self.status = config.willAppear
default:
break
}
}
}
init(_ title:String,
_ config:Config,
coreDataBooleanPropertyName:String) {
self.title = title
self.config = config
self.coreDataBooleanPropertyName = coreDataBooleanPropertyName
self.status = defaultStatus
switch coreDataBooleanPropertyName {
case "isSoft":
self.status = config.isSoft
case "turnedOn":
self.status = config.turnedOn
case "willAppear":
self.status = config.willAppear
default:
break
}
}
var body: some View {
Toggle(isOn: $status, label: {
ControlTitle(title)
})
.toggleStyle(CheckboxStyle())
.onChange(of: status, perform: { newStatus in
switch coreDataBooleanPropertyName {
case "isSoft":
config.isSoft = newStatus
case "turnedOn":
config.turnedOn = newStatus
config.willAppear = !newStatus
case "willAppear":
config.willAppear = newStatus
config.turnedOn = !newStatus
default:
return
}
let coreDataContext = PersistenceController.shared.context
do {
try coreDataContext.save()
}
catch let error{
print(error.localizedDescription)
}
})
struct CheckboxStyle: ToggleStyle {
func makeBody(configuration: Self.Configuration) -> some View {
return HStack {
Image(systemName: configuration.isOn ? "checkmark.circle.fill" : "circle")
.resizable()
.frame(width: 24, height: 24)
.foregroundColor(configuration.isOn ? Color.from("ffba00") : .gray)
.font(.system(size: 20, weight: .bold, design: .default))
.onTapGesture {
configuration.isOn.toggle()
}
configuration.label
Spacer()
}
}
}
I have tested the core data entry config and it works properly but the toggle switches willAppear and turnedOn do not update when one or the other are selected.
What am I missing?
Instead of didSet, which will not fire if just a property of your object changes, you could observe its changes via objectWillChange (note inline comments as well):
Toggle(isOn: $status, label: {
ControlTitle(title)
})
.onReceive(config.objectWillChange) { _ in
//run the code from your didSet here
//note that you *may* need to use something like DispatchQueue.main.async { } in here to get the updated values of the object since the values may not be updated until the next run loop -- I'm unsure of how this will behave with CoreData objects
}
.onChange(of: status, perform: { newStatus in
//etc
Related
I'm trying to implement a passcode view in iOS. I was following the guide here.
I'm trying to improve it a bit so it allows me to create a passcode by enter same passcode twice. I added a "state" property to the #StateObject and want to clear entered passcode after user input the passcode first time.
Here is my current code:
LockScreenModel.swift
====================
import Foundation
class LockScreenModel: ObservableObject {
#Published var pin: String = ""
#Published var showPin = false
#Published var isDisabled = false
#Published var state = LockScreenState.normal
}
enum LockScreenState: String, CaseIterable {
case new
case verify
case normal
case remove
}
====================
LockScreen.swift
====================
import SwiftUI
struct LockScreen: View {
#StateObject var lockScreenModel = LockScreenModel()
let initialState: LockScreenState
var handler: (String, LockScreenState, (Bool) -> Void) -> Void
var body: some View {
VStack(spacing: 40) {
Text(NSLocalizedString("lock.label.\(lockScreenModel.state.rawValue)", comment: "")).font(.title)
ZStack {
pinDots
backgroundField
}
showPinStack
}
.onAppear(perform: {lockScreenModel.state = initialState})
.onDisappear(perform: {
lockScreenModel.pin = ""
lockScreenModel.showPin = false
lockScreenModel.isDisabled = false
lockScreenModel.state = .normal
})
}
private var pinDots: some View {
HStack {
Spacer()
ForEach(0..<6) { index in
Image(systemName: self.getImageName(at: index))
.font(.system(size: 30, weight: .thin, design: .default))
Spacer()
}
}
}
private var backgroundField: some View {
let boundPin = Binding<String>(get: { lockScreenModel.pin }, set: { newValue in
if newValue.last?.isWholeNumber == true {
lockScreenModel.pin = newValue
}
self.submitPin()
})
return TextField("", text: boundPin, onCommit: submitPin)
.accentColor(.clear)
.foregroundColor(.clear)
.keyboardType(.numberPad)
.disabled(lockScreenModel.isDisabled)
}
private var showPinStack: some View {
HStack {
Spacer()
if !lockScreenModel.pin.isEmpty {
showPinButton
}
}
.frame(height: 20)
.padding([.trailing])
}
private var showPinButton: some View {
Button(action: {
lockScreenModel.showPin.toggle()
}, label: {
lockScreenModel.showPin ?
Image(systemName: "eye.slash.fill").foregroundColor(.primary) :
Image(systemName: "eye.fill").foregroundColor(.primary)
})
}
private func submitPin() {
guard !lockScreenModel.pin.isEmpty else {
lockScreenModel.showPin = false
return
}
if lockScreenModel.pin.count == 6 {
lockScreenModel.isDisabled = true
handler(lockScreenModel.pin, lockScreenModel.state) { isSuccess in
if isSuccess && lockScreenModel.state == .new {
lockScreenModel.state = .verify
lockScreenModel.pin = ""
lockScreenModel.isDisabled = false
} else if !isSuccess {
lockScreenModel.pin = ""
lockScreenModel.isDisabled = false
print("this has to called after showing toast why is the failure")
}
}
}
// this code is never reached under normal circumstances. If the user pastes a text with count higher than the
// max digits, we remove the additional characters and make a recursive call.
if lockScreenModel.pin.count > 6 {
lockScreenModel.pin = String(lockScreenModel.pin.prefix(6))
submitPin()
}
}
private func getImageName(at index: Int) -> String {
if index >= lockScreenModel.pin.count {
return "circle"
}
if lockScreenModel.showPin {
return lockScreenModel.pin.digits[index].numberString + ".circle"
}
return "circle.fill"
}
}
extension String {
var digits: [Int] {
var result = [Int]()
for char in self {
if let number = Int(String(char)) {
result.append(number)
}
}
return result
}
}
extension Int {
var numberString: String {
guard self < 10 else { return "0" }
return String(self)
}
}
====================
The problem is the line lockScreenModel.state = .verify. If I include this line, the passcode TextField won't get cleared, but if I remove this line, the passcode TextField is cleared.
If I add a breakpoint in set method of boundPin, I can see after set pin to empty and state to verify, the set method of boundPin is called with newValue of the old pin which I have no idea why. If I only set pin to empty but don't set state to verify, that set method of boundPin won't get called which confuse me even more. I can't figure out which caused this strange behavior.
I've got several activity indicators. The one in the choose subscription screen works ok, but when I do a similar thing when user taps a button that writes some information to a database, the activity indicator doesn't show after the boolean value is set to true for the isAnimating parameter.
When I set the isLoading variable to true in its declaration, the activity indicator appears.
Below is the code that does the database work, and the activity indicator view.
import SwiftUI
struct LanguagePickerWheel: View {
#State private var selectedLanguage: String = ""
#State private var isLoading = false
var availableLanguages: [String] = []
#Environment(\.presentationMode) var presentation
#Environment(\.managedObjectContext) private var viewContext
private func dismiss() {
self.presentation.wrappedValue.dismiss()
}
var body: some View {
GeometryReader { geometry in
VStack {
HStack {
Button("Cancel") { self.dismiss() }
.padding(.top)
.padding(.trailing, 125)
Button("Select") {
DispatchQueue.main.async {
isLoading = true
}
let queue = DispatchQueue(label: "work-queue-1")
queue.async {
if selectedLanguage == "" {
selectedLanguage = availableLanguages[0]
}
// submit language to the addAndSaveLanguage method
let newLanguage = Language(context: viewContext)
newLanguage.name = selectedLanguage
newLanguage.setAsRevision = false
PersistenceController.shared.saveDB()
do {
// This solution assumes you've got the file in your bundle
if let path = Bundle.main.path(forResource: "\(selectedLanguage)_English_Over_2500_Words", ofType: "txt") {
let data = try String(contentsOfFile:path, encoding: String.Encoding.utf8)
var arrayOfStrings: [String]
arrayOfStrings = data.components(separatedBy: ";")
for string in arrayOfStrings {
let newCommonWord = CommonWordThing(context: viewContext)
newCommonWord.native = string.components(separatedBy: "_")[1]
newCommonWord.foreign = string.components(separatedBy: "_")[0]
newCommonWord.ckimage = false
newCommonWord.inUse = false
newCommonWord.typingTestCorrect = 0
newCommonWord.arrangeWordsCorrect = 0
newCommonWord.ckreference = newLanguage.ckrecordname
newCommonWord.attempts = 0
newCommonWord.image = nil
newCommonWord.repetitionInterval = 0
newCommonWord.testsUntilPresented = 0
newCommonWord.setAsRevision = false
newCommonWord.language = newLanguage
var stringNumber = string.split(separator: "_")[2]
if stringNumber.contains("\r\n") {
stringNumber.removeLast(1)
}
newCommonWord.count = NumberFormatter().number(from: String(stringNumber) as String)?.int64Value ?? 0
}
}
} catch let err as NSError {
// do something with Error
print("Couldn't save new language to database: \(err)")
}
PersistenceController.shared.saveDB()
}
isLoading = false
self.dismiss()
}
.padding(.top)
.padding(.leading, 125)
}
Text("Choose a language:")
.font(.title)
.padding(.top, 50)
Picker("Choose a language:", selection: $selectedLanguage, content: {
ForEach(Array(availableLanguages), id: \.self) { language in
Text(language)
}
})
.pickerStyle(WheelPickerStyle())
.padding(.leading)
.padding(.trailing)
}
ActivityIndicatorView(isAnimating: $isLoading, text: "Loading...")
.position(x: geometry.frame(in: .local).midX, y: geometry.frame(in: .local).midY)
}
}
}
Activity Indicator:
struct ActivityIndicatorView: View {
#Binding var isAnimating:Bool
var body: some View {
if isAnimating{
ZStack{
RoundedRectangle(cornerRadius: 15).fill(Color.lightGray)
ProgressView {
Text("Loading...")
.font(.title2)
}
}.frame(width: 120, height: 120, alignment: .center)
.background(RoundedRectangle(cornerRadius: 25).stroke(.gray,lineWidth: 2))
}
}
}
Everything that happens between isLoading = true and isLoading = false in your Button("Select") action is running on the main thread and is therefore blocking the UI. The blocked UI cannot be updated so the UI will only update again when it reaches isLoading = false but then it will be hidden again.
You need to look into async/await and/or threading to solve your issue.
I have two alert which is called if the boolean is true.
Alert - 1 - It is called if there is any issues with the bluetooth state other than powered on.This is called directly from a swift package named BLE. The code snippet is below.
Alert - 2 - It is called when you want to unpair the peripheral giving the user two options.Unpair or remain on the same page.
Issue :
Both the alert seems to be working fine but if they are not placed in the same view. When I place the alert in the same view the last displayed alert is called from the sequence top to bottom.
The OS reads the first alert but only activates the second alert if it's called.
Is there a way to make both alert functional if they are called.
I referred to below solution but i was getting the same results.
Solution 1 and Solution 2
There are 2 Code snippets
1. Main Application
import SwiftUI
import BLE
struct Dashboard: View {
#EnvironmentObject var BLE: BLE
#State private var showUnpairAlert: Bool = false
private var topLayer: HeatPeripheral {
self.BLE.peripherals.baseLayer.top
}
var body: some View {
VStack(alignment: .center, spacing: 0) {
// MARK: - Menu Bar
VStack(alignment: .center, spacing: 4) {
Button(action: {
print("Unpair tapped!")
self.showUnpairAlert = true
}) {
HStack {
Text("Unpair")
.fontWeight(.bold)
.font(.body)
}
.frame(minWidth: 85, minHeight: 35)
.cornerRadius(30)
}
}
}
.onAppear(perform: {
self.BLE.update()
})
// Alert 1 - It is called if it meets one of the cases and returns the alert
// It is presented in the function centralManagerDidUpdateState
.alert(isPresented: $BLE.showStateAlert, content: { () -> Alert in
let state = self.BLE.centralManager!.state
var message = ""
switch state {
case .unknown:
message = "Bluetooth state is unknown"
case .resetting:
message = "Bluetooth is resetting..."
case .unsupported:
message = "This device doesn't have a bluetooth radio."
case .unauthorized:
message = "Turn On Bluetooth In The Settings App to Allow Battery to Connect to App."
case .poweredOff:
message = "Turn On Bluetooth to Allow Battery to Connect to App."
break
#unknown default:
break
}
return Alert(title: Text("Bluetooth is \(self.BLE.getStateString())"), message: Text(message), dismissButton: .default(Text("OK")))
})
// Alert 2 - It is called when you tap the unpair button
.alert(isPresented: $showUnpairAlert) {
Alert(title: Text("Unpair from \(checkForDeviceInformation())"), message: Text("*Your peripheral command will stay on."), primaryButton: .destructive(Text("Unpair")) {
self.unpairAndSetDefaultDeviceInformation()
}, secondaryButton: .cancel())
}
}
func unpairAndSetDefaultDeviceInformation() {
defaults.set(defaultDeviceinformation, forKey: Keys.deviceInformation)
disconnectPeripheral()
print("Pod unpaired and view changed to Onboarding")
self.presentationMode.wrappedValue.dismiss()
DispatchQueue.main.async {
self.activateLink = true
}
}
func disconnectPeripheral(){
if skiinBLE.peripherals.baseLayer.top.cbPeripheral != nil {
self.skiinBLE.disconnectPeripheral()
}
}
}
2. BLE Package
import SwiftUI
import Combine
import CoreBluetooth
public class BLE: NSObject, ObservableObject {
public var centralManager: CBCentralManager? = nil
public let baseLayerServices = "XXXXXXXXXXXXXXX"
let defaults = UserDefaults.standard
#Published public var showStateAlert: Bool = false
public func start() {
self.centralManager = CBCentralManager(delegate: self, queue: nil, options: nil)
self.centralManager?.delegate = self
}
public func getStateString() -> String {
guard let state = self.centralManager?.state else { return String() }
switch state {
case .unknown:
return "Unknown"
case .resetting:
return "Resetting"
case .unsupported:
return "Unsupported"
case .unauthorized:
return "Unauthorized"
case .poweredOff:
return "Powered Off"
case .poweredOn:
return "Powered On"
#unknown default:
return String()
}
}
}
extension BLE: CBCentralManagerDelegate {
public func centralManagerDidUpdateState(_ central: CBCentralManager) {
print("state: \(self.getStateString())")
if central.state == .poweredOn {
self.showStateAlert = false
if let connectedPeripherals = self.centralManager?.retrieveConnectedPeripherals(withServices: self.baseLayerServices), connectedPeripherals.count > 0 {
print("Already connected: \(connectedPeripherals.map{$0.name}), self.peripherals: \(self.peripherals)")
self.centralManager?.stopScan()
}
else {
print("scanForPeripherals")
self.centralManager?.scanForPeripherals(withServices: self.baseLayerServices, options: nil)
}
}
else {
self.showStateAlert = true // Alert is called if there is any issue with the state.
}
}
}
Thank You !!!
The thing to remember is that view modifiers don't really just modify a view, they return a whole new view. So the first alert modifier returns a new view that handles alerts in the first way. The second alert modifier returns a new view that modifies alerts the second way (overwriting the first method) and that's the only one that ultimately is in effect. The outermost modifier is what matters.
There are couple things you can try, first try attaching the different alert modifiers to two different view, not the same one.
Second you can try the alternate form of alert that takes a Binding of an optional Identifiable and passes that on to the closure. When value is nil, nothing happens. When the state of changes to something other than nil, the alert should appear.
Here's an example using the alert(item:) form as opposed to the Bool based alert(isPresented:).
enum Selection: Int, Identifiable {
case a, b, c
var id: Int { rawValue }
}
struct MultiAlertView: View {
#State private var selection: Selection? = nil
var body: some View {
HStack {
Button(action: {
self.selection = .a
}) { Text("a") }
Button(action: {
self.selection = .b
}) { Text("b") }
}.alert(item: $selection) { (s: Selection) -> Alert in
Alert(title: Text("selection: \(s.rawValue)"))
}
}
}
I need to create an alert with 3 buttons, but it looks like SwiftUI only gives us two options right now: one button or two buttons. I know that with UIKit, 3 buttons are achievable, but I can't seem to find a workaround in the latest version of SwiftUI to do this. Below is my code where I'm using only a primary and secondary button.
Button(action: {
self.showAlert = true
}){
Text("press me")
}
.alert(isPresented: self.$showAlert){
Alert(title: Text("select option"), message: Text("pls help"), primaryButton: Alert.Button.default(Text("yes"), action: {
print("yes clicked")
}), secondaryButton: Alert.Button.cancel(Text("no"), action: {
print("no clicked")
})
)
}
This is now possible on iOS 15/macOS 12 with a new version of the alert modifier: alert(_:isPresented:presenting:actions:).
It works a bit differently because the Alert struct isn't used anymore; you use regular SwiftUI Buttons instead. Add a ButtonRole to indicate which buttons perform the "cancel" action or "destructive" actions. Add the .keyboardShortcut(.defaultAction) modifier to a button to indicate it performs the principal action.
For example:
MyView()
.alert("Test", isPresented: $presentingAlert) {
Button("one", action: {})
Button("two", action: {}).keyboardShortcut(.defaultAction)
Button("three", role: .destructive, action: {})
Button("four", role: .cancel, action: {})
}
Creates the following alert:
.actionSheet, .sheet, and .popover are options to provide custom alerts. Consider this sample:
import SwiftUI
struct ContentView: View {
#State private var showModal = false
#State private var cnt = 0
var body: some View {
VStack {
Text("Counter is \(cnt)")
Button("Show alert") {
self.showModal = true
}
}
.sheet(isPresented: $showModal,
onDismiss: {
print(self.showModal)}) {
CustomAlert(message: "This is Modal view",
titlesAndActions: [("OK", {}),
("Increase", { self.cnt += 1 }),
("Cancel", nil)])
}
}
}
struct CustomAlert: View {
#Environment(\.presentationMode) var presentation
let message: String
let titlesAndActions: [(title: String, action: (() -> Void)?)] // = [.default(Text("OK"))]
var body: some View {
VStack {
Text(message)
Divider().padding([.leading, .trailing], 40)
HStack {
ForEach(titlesAndActions.indices, id: \.self) { i in
Button(self.titlesAndActions[i].title) {
(self.titlesAndActions[i].action ?? {})()
self.presentation.wrappedValue.dismiss()
}
.padding()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you want to be compatible with previous versions of iOS 15, consider to use actionSheet like this:
.actionSheet(isPresented: $showActionSheet) {
ActionSheet(title: Text("Title"), message: Text("Choose one of this three:"), buttons: [
.default(Text("First")) { },
.default(Text("Second")) { },
.default(Text("Third")) { },
.cancel()
])
}
example below requires iOS 14.0 and available for both iPad and iPhone.
i also beleive this code will require minimum modifications to support iOS 13.0 as well.
(xcode project with usage example is available here)
import SwiftUI
public extension View {
func alert(if condition: Binding<Bool>,
title: String = "",
text: String = "",
buttons: [Alert.Option] = [.cancel("OK")]) -> some View {
let alert: AlertView = .init(condition, title, text, buttons)
return overlay(condition.wrappedValue ? alert : nil)
}
}
private struct AlertView : View {
#Binding var active: Bool
private let title: String
private let text: String
private let buttons: [Alert.Option]
private let cancel: Alert.Option?
init(_ active: Binding<Bool>, _ title: String, _ text: String, _ buttons: [Alert.Option]) {
self._active = active
self.title = title
self.text = text
self.buttons = buttons.filter { $0.role != .cancel }
self.cancel = buttons.first { $0.role == .cancel } ?? (buttons.isEmpty ? .cancel("OK") : nil)
}
var body: some View {
VStack() {
if title.count > 0 { Text(title).fontWeight(.semibold).padding([.top, .bottom], 5) }
if text.count > 0 { Text(text).fixedSize(horizontal: false, vertical: true) }
ForEach(buttons, id: \.label) { Divider(); button(for: $0) }
if let cancel { Divider(); button(for: cancel) }
}.padding()
.frame(maxWidth: width)
.background(Color(.secondarySystemBackground))
.cornerRadius(20)
.shadow(color: Color(.black), radius: 5)
.screenCover()
.onTapGesture { if cancellable { active = false } }
}
private func button(for option: Alert.Option) -> some View {
HStack {
Spacer()
Text(option.label)
.foregroundColor(option.role == .destructive ? Color(.red) : Color(.link))
.fontWeight(option.role == .cancel ? .semibold : .regular)
.padding([.top, .bottom], 5)
Spacer()
}.background(Color(.secondarySystemBackground))
.onTapGesture { if option.role != .cancel { option.action() }; active = false }
}
private var width: CGFloat { min((UIScreen.main.bounds.width * 0.8), 500) }
private var cancellable: Bool { cancel != nil }
}
extension Alert {
public final class Option {
public let label: String
public let role: Role
public let action: () -> Void
public init(_ label: String, role: Role = .regular, action: #escaping () -> Void = { }) {
self.label = label
self.role = role
self.action = action
}
public static func cancel(_ label: String) -> Option { .init(label, role: .cancel) }
}
}
public extension SwiftUI.Alert.Option { enum Role { case regular; case cancel; case destructive } }
public extension View {
func screenCover(_ color: Color = .init(UIColor.systemBackground), opacity: Double = 0.5) -> some View {
color.opacity(opacity)
.ignoresSafeArea()
.overlay(self)
}
}
I want to change another unrelated #State variable when a Picker gets changed, but there is no onChanged and it's not possible to put a didSet on the pickers #State. Is there another way to solve this?
Deployment target of iOS 14 or newer
Apple has provided a built in onChange extension to View, which can be used like this:
struct MyPicker: View {
#State private var favoriteColor = 0
var body: some View {
Picker(selection: $favoriteColor, label: Text("Color")) {
Text("Red").tag(0)
Text("Green").tag(1)
}
.onChange(of: favoriteColor) { tag in print("Color tag: \(tag)") }
}
}
Deployment target of iOS 13 or older
struct MyPicker: View {
#State private var favoriteColor = 0
var body: some View {
Picker(selection: $favoriteColor.onChange(colorChange), label: Text("Color")) {
Text("Red").tag(0)
Text("Green").tag(1)
}
}
func colorChange(_ tag: Int) {
print("Color tag: \(tag)")
}
}
Using this helper
extension Binding {
func onChange(_ handler: #escaping (Value) -> Void) -> Binding<Value> {
return Binding(
get: { self.wrappedValue },
set: { selection in
self.wrappedValue = selection
handler(selection)
})
}
}
First of all, full credit to ccwasden for the best answer. I had to modify it slightly to make it work for me, so I'm answering this question hoping someone else will find it useful as well.
Here's what I ended up with (tested on iOS 14 GM with Xcode 12 GM)
struct SwiftUIView: View {
#State private var selection = 0
var body: some View {
Picker(selection: $selection, label: Text("Some Label")) {
ForEach(0 ..< 5) {
Text("Number \($0)") }
}.onChange(of: selection) { _ in
print(selection)
}
}
}
The inclusion of the "_ in" was what I needed. Without it, I got the error "Cannot convert value of type 'Int' to expected argument type '()'"
I think this is simpler solution:
#State private var pickerIndex = 0
var yourData = ["Item 1", "Item 2", "Item 3"]
// USE this if needed to notify parent
#Binding var notifyParentOnChangeIndex: Int
var body: some View {
let pi = Binding<Int>(get: {
return self.pickerIndex
}, set: {
self.pickerIndex = $0
// TODO: DO YOUR STUFF HERE
// TODO: DO YOUR STUFF HERE
// TODO: DO YOUR STUFF HERE
// USE this if needed to notify parent
self.notifyParentOnChangeIndex = $0
})
return VStack{
Picker(selection: pi, label: Text("Yolo")) {
ForEach(self.yourData.indices) {
Text(self.yourData[$0])
}
}
.pickerStyle(WheelPickerStyle())
.padding()
}
}
I know this is a year old post, but I thought this solution might help others that stop by for a visit in need of a solution. Hope it helps someone else.
import Foundation
import SwiftUI
struct MeasurementUnitView: View {
#State var selectedIndex = unitTypes.firstIndex(of: UserDefaults.standard.string(forKey: "Unit")!)!
var userSettings: UserSettings
var body: some View {
VStack {
Spacer(minLength: 15)
Form {
Section {
Picker(selection: self.$selectedIndex, label: Text("Current UnitType")) {
ForEach(0..<unitTypes.count, id: \.self) {
Text(unitTypes[$0])
}
}.onReceive([self.selectedIndex].publisher.first()) { (value) in
self.savePick()
}
.navigationBarTitle("Change Unit Type", displayMode: .inline)
}
}
}
}
func savePick() {
if (userSettings.unit != unitTypes[selectedIndex]) {
userSettings.unit = unitTypes[selectedIndex]
}
}
}
I use a segmented picker and had a similar requirement. After trying a few things I just used an object that had both an ObservableObjectPublisher and a PassthroughSubject publisher as the selection. That let me satisfy SwiftUI and with an onReceive() I could do other stuff as well.
// Selector for the base and radix
Picker("Radix", selection: $base.value) {
Text("Dec").tag(10)
Text("Hex").tag(16)
Text("Oct").tag(8)
}
.pickerStyle(SegmentedPickerStyle())
// receiver for changes in base
.onReceive(base.publisher, perform: { self.setRadices(base: $0) })
base has both an objectWillChange and a PassthroughSubject<Int, Never> publisher imaginatively called publisher.
class Observable<T>: ObservableObject, Identifiable {
let id = UUID()
let objectWillChange = ObservableObjectPublisher()
let publisher = PassthroughSubject<T, Never>()
var value: T {
willSet { objectWillChange.send() }
didSet { publisher.send(value) }
}
init(_ initValue: T) { self.value = initValue }
}
typealias ObservableInt = Observable<Int>
Defining objectWillChange isn't strictly necessary but when I wrote that I liked to remind myself that it was there.
For people that have to support both iOS 13 and 14, I added an extension which works for both. Don't forget to import Combine.
Extension View {
#ViewBuilder func onChangeBackwardsCompatible<T: Equatable>(of value: T, perform completion: #escaping (T) -> Void) -> some View {
if #available(iOS 14.0, *) {
self.onChange(of: value, perform: completion)
} else {
self.onReceive([value].publisher.first()) { (value) in
completion(value)
}
}
}
}
Usage:
Picker(selection: $selectedIndex, label: Text("Color")) {
Text("Red").tag(0)
Text("Blue").tag(1)
}.onChangeBackwardsCompatible(of: selectedIndex) { (newIndex) in
print("Do something with \(newIndex)")
}
Important note: If you are changing a published property inside an observed object within your completion block, this solution will cause an infinite loop in iOS 13. However, it is easily fixed by adding a check, something like this:
.onChangeBackwardsCompatible(of: showSheet, perform: { (shouldShowSheet) in
if shouldShowSheet {
self.router.currentSheet = .chosenSheet
showSheet = false
}
})
SwiftUI 1 & 2
Use onReceive and Just:
import Combine
import SwiftUI
struct ContentView: View {
#State private var selection = 0
var body: some View {
Picker("Some Label", selection: $selection) {
ForEach(0 ..< 5, id: \.self) {
Text("Number \($0)")
}
}
.onReceive(Just(selection)) {
print("Selected: \($0)")
}
}
}
iOS 14 and CoreData entities with relationships
I ran into this issue while trying to bind to a CoreData entity and found that the following works:
Picker("Level", selection: $contact.level) {
ForEach(levels) { (level: Level?) in
HStack {
Circle().fill(Color.green)
.frame(width: 8, height: 8)
Text("\(level?.name ?? "Unassigned")")
}
.tag(level)
}
}
.onChange(of: contact.level) { _ in savecontact() }
Where "contact" is an entity with a relationship to "level".
The Contact class is an #ObservedObject var contact: Contact
saveContact is a do-catch function to try viewContext.save()...
The very important issue : we must pass something to "tag" modifier of Picker item view (inside ForEach) to let it "identify" items and trigger selection change event. And the value we passed will return to Binding variable with "selection" of Picker.
For example :
Picker(selection: $selected, label: Text("")){
ForEach(data){item in //data's item type must conform Identifiable
HStack{
//item view
}
.tag(item.property)
}
}
.onChange(of: selected, perform: { value in
//handle value of selected here (selected = item.property when user change selection)
})