How to make a drag and drop viewmodifier accept any object - swiftui

I have created a drag-and-drop viewmodifier that works as expected, but now I would like to make it accept any object. I can add <T: Identifiable> to all the functions, structs, and view-modifiers, but when I try to do add it to my singleton class, I get "Static stored properties not supported in generic types".
I need the singleton class, so I can put the .dropObjectOutside viewmodifier anywhere in my view-hierarchy, so I've tried downcasting the ID to a String, but I can't seem to make that work.
Is there a way to downcast or make this code accept any object?
import SwiftUI
// I want this to be any object
struct StopContent: Identifiable {
var id: String = UUID().uuidString
}
// Singleton class to hold drag state
class DragToReorderController: ObservableObject {
// Make it a singleton, so it can be accessed from any view
static let shared = DragToReorderController()
private init() { }
#Published var draggedID: String? // How do I make this a T.ID or downcast T.ID to string everywhere else?
#Published var dragActive:Bool = false
}
// Add ViewModifier to view
extension View {
func dragToReorder(_ item: StopContent, array: Binding<[StopContent]>) -> some View {
self.modifier(DragToReorderObject(sourceItem: item, contentArray: array))
}
func dropOutside() -> some View {
self.onDrop(of: [UTType.text], delegate: DropObjectOutsideDelegate())
}
}
import UniformTypeIdentifiers
// MARK: View Modifier
struct DragToReorderObject: ViewModifier {
let sourceItem: StopContent
#Binding var contentArray: [StopContent]
#ObservedObject private var dragReorder = DragToReorderController.shared
func body(content: Content) -> some View {
content
.onDrag {
dragReorder.draggedID = sourceItem.id
dragReorder.dragActive = false
return NSItemProvider(object: String(sourceItem.id) as NSString)
}
.onDrop(of: [UTType.text], delegate: DropObjectDelegate(sourceItem: sourceItem, listData: $contentArray, draggedItem: $dragReorder.draggedID, dragActive: $dragReorder.dragActive))
.onChange(of: dragReorder.dragActive, perform: { value in
if value == false {
// Drag completed
}
})
.opacity(dragReorder.draggedID == sourceItem.id && dragReorder.dragActive ? 0 : 1)
}
}
// MARK: Drop and reorder
struct DropObjectDelegate: DropDelegate {
let sourceItem: StopContent
#Binding var listData: [StopContent]
#Binding var draggedItem: String?
#Binding var dragActive: Bool
func dropEntered(info: DropInfo) {
if draggedItem == nil { draggedItem = sourceItem.id }
dragActive = true
// Make sure the dragged item has moved and that it still exists
if sourceItem.id != draggedItem {
if let draggedItemValid = draggedItem {
if let from = listData.firstIndex(where: { $0.id == draggedItemValid } ) {
// If that is true, move it to the new location
let to = listData.firstIndex(where: { $0.id == sourceItem.id } )!
if listData[to].id != draggedItem! {
listData.move(fromOffsets: IndexSet(integer: from),
toOffset: to > from ? to + 1 : to)
}
}
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
dragActive = false
draggedItem = nil
return true
}
}
// MARK: Drop and cancel
struct DropObjectOutsideDelegate: DropDelegate {
// Using a singleton so we can drop anywhere
#ObservedObject private var dragReorder = DragToReorderController.shared
func dropEntered(info: DropInfo) {
dragReorder.dragActive = true
}
func performDrop(info: DropInfo) -> Bool {
dragReorder.dragActive = false
dragReorder.draggedID = nil
return true
}
}

For this, you have to add Identifiable generic constraint everywhere. Also, use Int for draggedID instead of String.
Here is the demo code.
// Singleton class to hold drag state
class DragToReorderController: ObservableObject {
// Make it a singleton, so it can be accessed from any view
static let shared = DragToReorderController()
private init() { }
#Published var draggedID: Int?
#Published var dragActive: Bool = false
}
// Add ViewModifier to view
extension View {
func dragToReorder<T: Identifiable>(_ item: T, array: Binding<[T]>) -> some View {
self.modifier(DragToReorderObject(sourceItem: item, contentArray: array))
}
func dropOutside() -> some View {
self.onDrop(of: [UTType.text], delegate: DropObjectOutsideDelegate())
}
}
import UniformTypeIdentifiers
// MARK: View Modifier
struct DragToReorderObject<T: Identifiable>: ViewModifier {
let sourceItem: T
#Binding var contentArray: [T]
#ObservedObject private var dragReorder = DragToReorderController.shared
func body(content: Content) -> some View {
content
.onDrag {
dragReorder.draggedID = sourceItem.id.hashValue
dragReorder.dragActive = false
return NSItemProvider(object: String(sourceItem.id.hashValue) as NSString)
}
.onDrop(of: [UTType.text], delegate: DropObjectDelegate(sourceItem: sourceItem, listData: $contentArray, draggedItem: $dragReorder.draggedID, dragActive: $dragReorder.dragActive))
.onChange(of: dragReorder.dragActive, perform: { value in
if value == false {
// Drag completed
}
})
.opacity((dragReorder.draggedID == sourceItem.id.hashValue) && dragReorder.dragActive ? 0 : 1)
}
}
// MARK: Drop and reorder
struct DropObjectDelegate<T: Identifiable>: DropDelegate {
let sourceItem: T
#Binding var listData: [T]
#Binding var draggedItem: Int?
#Binding var dragActive: Bool
func dropEntered(info: DropInfo) {
if draggedItem == nil { draggedItem = sourceItem.id.hashValue }
dragActive = true
// Make sure the dragged item has moved and that it still exists
if sourceItem.id.hashValue != draggedItem {
if let draggedItemValid = draggedItem {
if let from = listData.firstIndex(where: { $0.id.hashValue == draggedItemValid } ) {
// If that is true, move it to the new location
let to = listData.firstIndex(where: { $0.id == sourceItem.id } )!
if listData[to].id.hashValue != draggedItem! {
listData.move(fromOffsets: IndexSet(integer: from),
toOffset: to > from ? to + 1 : to)
}
}
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
dragActive = false
draggedItem = nil
return true
}
}
// MARK: Drop and cancel
struct DropObjectOutsideDelegate: DropDelegate {
// Using a singleton so we can drop anywhere
#ObservedObject private var dragReorder = DragToReorderController.shared
func dropEntered(info: DropInfo) {
dragReorder.dragActive = true
}
func performDrop(info: DropInfo) -> Bool {
dragReorder.dragActive = false
dragReorder.draggedID = nil
return true
}
}

Related

Why is the navigationDestination with the binding not updating properly?

Why is the binding not updating the array and the PatientView title in the ContentView?
I use the ForEach that takes a binding and the navigationDestination then passes that binding to the child view.
import SwiftUI
struct PatientView: View {
#Binding var patient: Patient
var body: some View {
Section("Name") {
TextField("Name", text: $patient.name)
.textFieldStyle(.roundedBorder)
}
Spacer()
.navigationTitle(patient.name)
}
}
protocol BlankInit {
init()
}
extension View {
func binding<V>(id: V.ID, array: Binding<[V]>) -> Binding<V> where V: Identifiable {
if let ret = array.first(where: { $0.id == id}) {
return ret
} else {
fatalError()
}
}
}
extension Binding : Hashable, Equatable where Value: Identifiable, Value: Equatable, Value: Hashable {
public static func == (lhs: Binding<Value>, rhs: Binding<Value>) -> Bool {
lhs.wrappedValue == rhs.wrappedValue
}
public func hash(into hasher: inout Hasher) {
hasher.combine("Binding")
hasher.combine(self.wrappedValue)
}
}
struct Patient: Hashable, Equatable, Identifiable, BlankInit {
var id: UUID = UUID()
var name: String = ""
var age: UInt8 = 0
init(_ name: String, age: UInt8 = 0) {
self.name = name
self.age = 0
self.id = UUID()
}
init() {
self.name = ""
self.age = 0
self.id = UUID()
}
static func == (lhs: Patient, rhs: Patient) -> Bool {
lhs.id == rhs.id && lhs.name == rhs.name && lhs.age == rhs.age
}
}
struct ContentView: View {
#StateObject var appData: AppData = AppData()
var body: some View {
NavigationStack(path: $appData.path) {
List {
ForEach($appData.patients) { $pt in
NavigationLink(pt.name, value: $pt)
}
}
.navigationDestination(for: Binding<Patient>.self) { item in
PatientView(patient: item)
}
.navigationTitle("Patients")
}
.environmentObject(appData)
.padding()
}
}
class AppData: ObservableObject {
#Published var path = NavigationPath()
#Published var patients = [Patient]()
init() {
patients.append(Patient("Smith"))
patients.append(Patient("Jones"))
patients.append(Patient("DeCaro"))
}
}
#main
struct TestAppApp: App {
var body: some Scene {
WindowGroup {
ContentView() //.environmentObject(AppData())
}
}
}
It works fine if I do the following:
struct ContentView: View {
#StateObject var appData: AppData = AppData()
var body: some View {
NavigationStack(path: $appData.path) {
List {
ForEach(appData.patients) { pt in
NavigationLink(pt.name, value: pt)
}
}
.navigationDestination(for: Patient.self) { item in
let binding = binding(id: item.id, array: $appData.patients)
PatientView(patient: binding)
}
.navigationTitle("Patients")
}
.environmentObject(appData)
.padding()
}
}
It seems more 'elegant' and less kludgy to be able to do the Binding version????

SwiftUI - #State property struct with binding doesn't cause redraw

Below is my code to draw a SwiftUI view that use a publisher to get its items that need drawing in a list. The items all have boolean values drawn with a Toggle.
My view is dumb so I can use any type of boolean value, perhaps UserDefaults backed, core data backed, or simply a boolean property somewhere... anyway, this doesn't redraw when updating a bool outside of the view when one of the booleans is updated.
The onReceive is called and I can see the output change in my console, but binding isn't a part of my struct of ToggleItem and so SwiftUI doesn't redraw.
My code...
I have a struct that looks like this, note the binding type here...
struct ToggleItem: Identifiable, Equatable {
let id: String
let name: String
let isOn: Binding<Bool>
public static func == (lhs: ToggleItem, rhs: ToggleItem) -> Bool {
lhs.id == rhs.id
}
}
And in my SwiftUI I have this...
struct MyView: View {
#State private var items: [ToggleItem] = []
let itemsPublisher: AnyPublisher<[ToggleItem], Never>
// ...
var body: some View {
List {
// ...
}
.onReceive(itemsPublisher) { newItems in
print("New items: \(newItems)")
items.removeAll() // hacky redraw
items = newItems
}
}
I can see what's going on here, as Binding<Bool> isn't a value, so SwiftUI sees the array of newItems equal to the items it's already drawn, as a result, this doesn't redraw.
Is there something I'm missing, perhaps some ingenious bit of SwiftUI/Combine that redraws this for me?
how about doing something like this instead to keep one source of truth:
struct ToggleItem: Identifiable, Equatable {
let id: String
let name: String
var isOn: Bool
public static func == (lhs: ToggleItem, rhs: ToggleItem) -> Bool {
lhs.id == rhs.id
}
}
class ItemsPublisher: ObservableObject {
#Published var items: [ToggleItem] = [ToggleItem(id: "1", name: "name1", isOn: false)] // for testing
}
struct ContentView: View {
#ObservedObject var itemsPublisher = ItemsPublisher() // to be passed in from parent
var body: some View {
VStack {
Button("Add item") {
let randomString = UUID().uuidString
let randomBool = Bool.random()
itemsPublisher.items.append(ToggleItem(id: randomString, name: randomString, isOn: randomBool))
}
List ($itemsPublisher.items) { $item in
Toggle(isOn: $item.isOn) {
Text(item.name)
}
}
Spacer()
}.padding(.top, 50)
.onReceive(itemsPublisher.items.publisher) { newItem in
print("----> new item: \(newItem)")
}
}
}
It seems as though removing the bindings is the right way forward!
Looking at the docs this makes sense now https://developer.apple.com/documentation/swiftui/binding
Use a binding to create a two-way connection between a property that stores data, and a view that displays and changes the data.
Although not explicit, this does suggest that the binding "property wrapper" is only to be used as a property in a SwiftUI view rather than a data model.
My changes
I added a closure to my view
let itemDidToggle: (ToggleItem, Bool) -> Void
and this is called in the Toggle binding's set() function which updates the value outside of the view, keeping the view dumb. This triggers the publisher to get called and update my stack. This coupled with updating the == equatable function to include the isOn property makes everything work...
My code
import UIKit
import PlaygroundSupport
import Combine
import SwiftUI
public struct MyItem {
public let identifier: String
public let description: String
}
public class ItemsManager {
public private(set) var items: [MyItem]
public let itemsPublisher: CurrentValueSubject<[MyItem], Never>
private let userDefaults: UserDefaults
public init(items: [MyItem], userDefaults: UserDefaults = .standard) {
self.userDefaults = userDefaults
self.items = items
self.itemsPublisher = .init(items)
}
public func isItemEnabled(identifier: String) -> Bool {
guard let item = item(for: identifier) else {
return false
}
if let isOnValue = userDefaults.object(forKey: item.identifier) as? NSNumber {
return isOnValue.boolValue
} else {
return false
}
}
public func setEnabled(_ isEnabled: Bool, forIdentifier identifier: String) {
userDefaults.set(isEnabled, forKey: identifier)
itemsPublisher.send(items)
}
func item(for identifier: String) -> MyItem? {
return items.first { $0.identifier == identifier }
}
}
struct MyView: View {
#State private var items: [ToggleItem] = []
let itemsPublisher: AnyPublisher<[ToggleItem], Never>
let itemDidToggle: (ToggleItem, Bool) -> Void
public init(
itemsPublisher: AnyPublisher<[ToggleItem], Never>,
itemDidToggle: #escaping (ToggleItem, Bool) -> Void
) {
self.itemsPublisher = itemsPublisher
self.itemDidToggle = itemDidToggle
}
var body: some View {
List {
Section(header: Text("Items")) {
ForEach(items) { item in
Toggle(
item.name,
isOn: .init(
get: { item.isOn },
set: { itemDidToggle(item, $0) }
)
)
}
}
}
.animation(.default, value: items)
.onReceive(itemsPublisher) { newItems in
print("New items: \(newItems)")
items = newItems
}
}
struct ToggleItem: Swift.Identifiable, Equatable {
let id: String
let name: String
let isOn: Bool
public static func == (lhs: ToggleItem, rhs: ToggleItem) -> Bool {
lhs.id == rhs.id && lhs.isOn == rhs.isOn
}
}
}
let itemsManager = ItemsManager(items: (1...10).map { .init(identifier: UUID().uuidString, description: "item \($0)") })
let publisher = itemsManager.itemsPublisher
.map { myItems in
myItems.map { myItem in
MyView.ToggleItem(id: myItem.identifier, name: myItem.description, isOn: itemsManager.isItemEnabled(identifier: myItem.identifier))
}
}
.eraseToAnyPublisher()
let view = MyView(itemsPublisher: publisher) { item, newValue in
itemsManager.setEnabled(newValue, forIdentifier: item.id)
}

Keyboard dismiss not working properly when dismiss the view swiftui

I'm trying to dismiss the keyboard when view dismissed but keyboard not dismiss properly, they only hide button but they showing height. More detail please check my code and screenshot.
Keyboard shows properly
Go To next Screen when keyboard open and dismiss
Keyboard button hide but show height with background
import SwiftUI
struct ContentView: View {
#State private var titleDtr: String = ""
#State private var clearAllText: Bool = false
#State var updateKeyboard = true
#State var userProfiles = [""]
var body: some View {
NavigationView{
HStack{
CustomTxtfldForFollowerScrn(isDeleteAcntScreen: .constant(false), text: $titleDtr, clearAllText: $clearAllText, isFirstResponder: $updateKeyboard, completion: { (reponse) in
if titleDtr.count >= 3 {
userProfiles.append(titleDtr)
}else if titleDtr.count <= 3 {
userProfiles.removeAll()
}
}).background(Color.red)
VStack(spacing:0) {
List {
if userProfiles.count > 0 {
ForEach(userProfiles.indices, id: \.self) { indexs in
NavigationLink(destination: ShowLoadingText()) {
Text(userProfiles[indexs]).foregroundColor(.blue)
}
}
}
}
}
}.onDisappear{
updateKeyboard = false
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
struct ShowLoadingText: View {
var body: some View {
ZStack {
VStack(alignment:.center, spacing: 15) {
HStack(spacing:10){
Group {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: Color.white))
Text("Loading More Users...")
}
}.foregroundColor(.black)
}
}
}
}
struct CustomTxtfldForFollowerScrn: UIViewRepresentable {
#Binding var isDeleteAcntScreen: Bool
#Binding var text: String
#Binding var clearAllText: Bool
#Binding var isFirstResponder: Bool
var completion: (String) -> Void
func makeUIView(context: UIViewRepresentableContext<CustomTxtfldForFollowerScrn>) -> UITextField {
let textField = UITextField(frame: .zero)
textField.text = text
textField.delegate = context.coordinator
textField.backgroundColor = .clear
if isDeleteAcntScreen {
textField.placeholder = "DELETE"
}else{
textField.placeholder = "Username"
}
textField.returnKeyType = .default
textField.textColor = .black
return textField
}
func makeCoordinator() -> CustomTxtfldForFollowerScrn.Coordinator {
return Coordinator(self)
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTxtfldForFollowerScrn>) {
uiView.text = text
if isFirstResponder && !context.coordinator.didBecomeFirstResponder {
uiView.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}else if clearAllText {
DispatchQueue.main.async {
uiView.text! = ""
text = ""
clearAllText = false
}
}else if !isFirstResponder {
DispatchQueue.main.async {
UIApplication.shared.endEditing()
}
}
}
class Coordinator: NSObject, UITextFieldDelegate {
var didBecomeFirstResponder = false
var parent: CustomTxtfldForFollowerScrn
init(_ view: CustomTxtfldForFollowerScrn) {
self.parent = view
}
func textFieldDidChangeSelection(_ textField: UITextField) {
DispatchQueue.main.async {
self.parent.text = textField.text ?? ""
self.parent.completion(textField.text!)
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
}
}
import UIKit
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
Here is the alternative way to dismissing the keyboard.
First, remove the main queue.
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTxtfldForFollowerScrn>) {
uiView.text = text
if isFirstResponder && !context.coordinator.didBecomeFirstResponder {
uiView.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}else if clearAllText {
DispatchQueue.main.async {
uiView.text! = ""
text = ""
clearAllText = false
}
}else if !isFirstResponder { // < -- From here
UIApplication.shared.endEditing()
}
}
and then update updateKeyboard on ShowLoadingText() appear.
ForEach(userProfiles.indices, id: \.self) { indexs in
NavigationLink(destination: ShowLoadingText().onAppear() { updateKeyboard = false }) { // <-- Here
Text(userProfiles[indexs]).foregroundColor(.blue)
}
}
Remove onDisappear code.

SwiftUI NavigationLink breaks after removing then inserting new ForEach element

For some reason, my NavigationLink is breaking in a specific circumstance:
Given the code below, here's the steps to reproduce:
Tap Sign In, which inserts an account into the list
Hit back to pop the stack
Swipe left and Delete, which removes the first element of the list
Tap Sign In again (should push onto the stack but does not)
Tap the first row (should push onto the stack but does not)
Here's the code:
import SwiftUI
class Account: ObservableObject, Identifiable, Equatable, Hashable {
let id: String
init(id: String) {
self.id = id
}
static func == (lhs: Account, rhs: Account) -> Bool {
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
class AccountManager: ObservableObject {
#Published private (set) var isLoading: Bool = false
#Published private (set) var accounts: [Account] = []
init() {
load()
}
func load() {
isLoading = true
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
self.accounts = [ Account(id: UUID().uuidString) ]
self.isLoading = false
}
}
func add(account: Account) {
accounts.insert(account, at: 0)
}
func delete(at offsets: IndexSet) {
accounts.remove(atOffsets: offsets)
}
}
struct AccountManagerEnvironmentKey: EnvironmentKey {
static var defaultValue: AccountManager = AccountManager()
}
extension EnvironmentValues {
var accountManager: AccountManager {
get { return self[AccountManagerEnvironmentKey.self] }
set { self[AccountManagerEnvironmentKey.self] = newValue }
}
}
struct ContentView: View {
#Environment(\.accountManager) var accountManager
#State var isLoading: Bool = false
#State var accounts: [Account] = []
#State var selectedAccount: Account? = nil
var body: some View {
NavigationView() {
ZStack {
List {
ForEach(accounts) { account in
NavigationLink(
destination: Text(account.id),
tag: account,
selection: $selectedAccount
) {
Text(account.id)
}
}
.onDelete(perform: { offsets in
accountManager.delete(at: offsets)
})
}
if isLoading {
ProgressView("Loading...")
}
}
.navigationBarTitle("Accounts", displayMode: .inline)
.toolbar(content: {
ToolbarItem(placement: .primaryAction) {
Button("Sign In") {
let newAccount = Account(id: UUID().uuidString)
accountManager.add(account: newAccount)
selectedAccount = newAccount
}
}
})
.onReceive(accountManager.$isLoading) { value in
isLoading = value
}
.onReceive(accountManager.$accounts) { value in
accounts = value
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
If I change the button action to do this, it works:
accountManager.add(account: newAccount)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
selectedAccount = newAccount
}
But that seems like a massive hack.

Cannot update value when wrapping SwiftUI binding in another binding

I want to make a login code screen. This consists of 4 separate UITextField elements, each accepting one character. What I did is implement a system whereby every time one of the UITextField's changes it will verify if all the values are filled out, and if they are update a boolean binding to tell the parent object that the code is correct.
In order to do this I wrap the #State variables inside a custom binding that does a callback on the setter, like this:
#State private var chars:[String] = ["","","",""]
...
var body: some View {
var bindings:[Binding<String>] = []
for x in 0..<self.chars.count {
let b = Binding<String>(get: {
return self.chars[x]
}, set: {
self.chars[x] = $0
self.validateCode()
})
bindings.append(b)
}
and those bindings are passed to the components. Every time my text value changes validateCode is called. This works perfectly.
However now I want to add an extra behavior: If the user types 4 characters and the code is wrong I want to move the first responder back to the first textfield and clear its contents. The first responder part works fine (I also manage that using #State variables, but I do not use a binding wrapper for those), however I can't change the text inside my code. I think it's because my components use that wrapped binding, and not the variable containing the text.
This is what my validateCode looks like:
func validateCode() {
let combinedCode = chars.reduce("") { (result, string) -> String in
return result + string
}
self.isValid = value == combinedCode
if !isValid && combinedCode.count == chars.count {
self.hasFocus = [true,false,false,false]
self.chars = ["","","",""]
}
}
hasFocus does its thing correctly and the cursor is being moved to the first UITextField. The text however remains in the text fields. I tried creating those bindings in the init so I could also use them in my validateCode function but that gives all kinds of compile errors because I am using self inside the getter and the setter.
Any idea how to solve this? Should I work with Observables? I'm just starting out with SwiftUI so it's possible I am missing some tools that I can use for this.
For completeness, here is the code of the entire file:
import SwiftUI
struct CWCodeView: View {
var value:String
#Binding var isValid:Bool
#State private var chars:[String] = ["","","",""]
#State private var hasFocus = [true,false,false,false]
#State private var nothingHasFocus:Bool = false
init(value:String,isValid:Binding<Bool>) {
self.value = value
self._isValid = isValid
}
func validateCode() {
let combinedCode = chars.reduce("") { (result, string) -> String in
return result + string
}
self.isValid = value == combinedCode
if !isValid && combinedCode.count == chars.count {
self.hasFocus = [true,false,false,false]
self.nothingHasFocus = false
self.chars = ["","","",""]
}
}
var body: some View {
var bindings:[Binding<String>] = []
for x in 0..<self.chars.count {
let b = Binding<String>(get: {
return self.chars[x]
}, set: {
self.chars[x] = $0
self.validateCode()
})
bindings.append(b)
}
return GeometryReader { geometry in
ScrollView (.vertical){
VStack{
HStack {
CWNumberField(letter: bindings[0],hasFocus: self.$hasFocus[0], previousHasFocus: self.$nothingHasFocus, nextHasFocus: self.$hasFocus[1])
CWNumberField(letter: bindings[1],hasFocus: self.$hasFocus[1], previousHasFocus: self.$hasFocus[0], nextHasFocus: self.$hasFocus[2])
CWNumberField(letter: bindings[2],hasFocus: self.$hasFocus[2], previousHasFocus: self.$hasFocus[1], nextHasFocus: self.$hasFocus[3])
CWNumberField(letter: bindings[3],hasFocus: self.$hasFocus[3], previousHasFocus: self.$hasFocus[2], nextHasFocus: self.$nothingHasFocus)
}
}
.frame(width: geometry.size.width)
.frame(height: geometry.size.height)
.modifier(AdaptsToSoftwareKeyboard())
}
}
}
}
struct CWCodeView_Previews: PreviewProvider {
static var previews: some View {
CWCodeView(value: "1000", isValid: .constant(false))
}
}
struct CWNumberField : View {
#Binding var letter:String
#Binding var hasFocus:Bool
#Binding var previousHasFocus:Bool
#Binding var nextHasFocus:Bool
var body: some View {
CWSingleCharacterTextField(character:$letter,hasFocus: $hasFocus, previousHasFocus: $previousHasFocus, nextHasFocus: $nextHasFocus)
.frame(width: 46,height:56)
.keyboardType(.numberPad)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.init("codeBorder"), lineWidth: 1)
)
}
}
struct CWSingleCharacterTextField : UIViewRepresentable {
#Binding var character: String
#Binding var hasFocus:Bool
#Binding var previousHasFocus:Bool
#Binding var nextHasFocus:Bool
func makeUIView(context: Context) -> UITextField {
let textField = UITextField.init()
//textField.isSecureTextEntry = true
textField.keyboardType = .numberPad
textField.delegate = context.coordinator
textField.textAlignment = .center
textField.font = UIFont.systemFont(ofSize: 16)
textField.tintColor = .black
textField.text = character
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
if hasFocus {
DispatchQueue.main.async {
uiView.becomeFirstResponder()
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator : NSObject, UITextFieldDelegate {
var parent:CWSingleCharacterTextField
init(_ parent:CWSingleCharacterTextField) {
self.parent = parent
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let result = (textField.text! as NSString).replacingCharacters(in: range, with: string)
if result.count > 0 {
DispatchQueue.main.async{
self.parent.hasFocus = false
self.parent.nextHasFocus = true
}
} else {
DispatchQueue.main.async{
self.parent.hasFocus = false
self.parent.previousHasFocus = true
}
}
if result.count <= 1 {
parent.character = string
return true
}
return false
}
}
}
Thanks!
you just make a little mistake, but i cannot believe you just "started" SwiftUI ;)
1.) just build textfield one time, so i took it as a member variable instead of building always a new one
2.) update the text in updateuiview -> that's it
3.) ...nearly: there is still a focus/update problem...the last of the four textfields won't update correctly ...i assume this is a focus problem....
try this:
struct CWSingleCharacterTextField : UIViewRepresentable {
#Binding var character: String
#Binding var hasFocus:Bool
#Binding var previousHasFocus:Bool
#Binding var nextHasFocus:Bool
let textField = UITextField.init()
func makeUIView(context: Context) -> UITextField {
//textField.isSecureTextEntry = true
textField.keyboardType = .numberPad
textField.delegate = context.coordinator
textField.textAlignment = .center
textField.font = UIFont.systemFont(ofSize: 16)
textField.tintColor = .black
textField.text = character
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = character
if hasFocus {
DispatchQueue.main.async {
uiView.becomeFirstResponder()
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator : NSObject, UITextFieldDelegate {
var parent:CWSingleCharacterTextField
init(_ parent:CWSingleCharacterTextField) {
self.parent = parent
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let result = (textField.text! as NSString).replacingCharacters(in: range, with: string)
if result.count > 0 {
DispatchQueue.main.async{
self.parent.hasFocus = false
self.parent.nextHasFocus = true
}
} else {
DispatchQueue.main.async{
self.parent.hasFocus = false
self.parent.previousHasFocus = true
}
}
if result.count <= 1 {
parent.character = string
return true
}
return false
}
}
}