How do I use TextInput for WatchOS in swiftUI - watchkit

Typically I would use presentTextInputControllerWithSuggestions() to show the TextInput field. But this isn't available in swiftUI because it is a function of WKInterfaceController. Do I have to use the WKInterfaceController for this?
I couldn't find anything in the documentation.

You can use extension for View in SwiftUI:
extension View {
typealias StringCompletion = (String) -> Void
func presentInputController(withSuggestions suggestions: [String], completion: #escaping StringCompletion) {
WKExtension.shared()
.visibleInterfaceController?
.presentTextInputController(withSuggestions: suggestions,
allowedInputMode: .plain) { result in
guard let result = result as? [String], let firstElement = result.first else {
completion("")
return
}
completion(firstElement)
}
}
}
Example:
struct ContentView: View {
var body: some View {
Button(action: {
presentInputController()
}, label: {
Text("Press this button")
})
}
private func presentInputController() {
presentInputController(withSuggestions: []) { result in
// handle result from input controller
}
}
}

This would be done through a TextField in SwiftUI.

Related

How do I make a Button in SwiftUi trigger a function in the UIKit wrapper?

I can add MKPointAnnotations by Long Tap Gesture to my Mapkit MapView.
Now I want to remove those markers by pressing a Button in Swift UI.
My idea was to set a variable true when the button is pressed and use this variable as condition for a function in the updateUIView function. But I get the error message that i can't refer to this variable in this nested function.
Here's a snippet from my addAnnotations wrapper. It works fine. How can I implement the removeAnnotation function on my mapView?
func makeUIView(context: Context) -> MKMapView {
let map = MKMapView()
map.setRegion(region, animated: false)
map.showsUserLocation = true
map.delegate = context.coordinator
locationManager.delegate = context.coordinator
let longPress = UILongPressGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.addAnnotation(gesture:)))
longPress.minimumPressDuration = 1
map.addGestureRecognizer(longPress)
return map
}
class Coordinator: NSObject, MKMapViewDelegate, CLLocationManagerDelegate {
var mapView: MapView
init(mapView: MapView) {
self.mapView = mapView
}
#objc func addAnnotation(gesture: UIGestureRecognizer) {
if gesture.state == .ended {
if let mapView = gesture.view as? MKMapView {
let point = gesture.location(in: mapView)
let locationCoordinate = mapView.convert(point, toCoordinateFrom: mapView)
let myPin = MKPointAnnotation()
myPin.title = "Latitude: \(locationCoordinate.latitude), Longitude: \(locationCoordinate.longitude)"
myPin.coordinate = locationCoordinate
mapView.addAnnotation(myPin)
}
}
}
Firstly, I don't know why you chose to use MapKit with UIKit in a SwiftUI project I assume, when you can do it all in SwiftUI unless you intend to support iOS 13 but anyways here is how you do it:
You have your imports of course:
import SwiftUI
import CoreLocation
import MapKit
Then create a viewModel like so:
class MapViewModel: ObservableObject {
#Published var didPressButton = false
}
Add an #StateObject property wrapper(avoid using #ObservevedObject if you are creating the instance in your ContentView)
And yes you can also use #EnvironmentObject
struct ContentView: View {
#StateObject var viewModel = MapViewModel()
var body: some View {
ZStack {
MapView(viewModel: viewModel)
Button("Perform Action") {
viewModel.didPressButton.toggle()
}
}
}
}
Then create an #ObservableObject which will share the same instance of MapViewModel from your ContentView
struct MapView: UIViewRepresentable {
#ObservableObject var viewModel: MapViewModel
func makeCoordinator() -> Coordinator {
return Coordinator()
}
func updateUIView(_ uiView: MKMapView, context: Context) {
if viewModel.didPressButton == true {
context.coordinator.performActionFromSwiftUI()
}
}
func makeUIView(context: Context) -> MKMapView {
let map = context.coordinator.mapView
map.setRegion(region, animated: false)
map.showsUserLocation = true
map.delegate = context.coordinator
locationManager.delegate = context.coordinator
let longPress = UILongPressGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.addAnnotation(gesture:)))
longPress.minimumPressDuration = 1
map.addGestureRecognizer(longPress)
return map
}
}
In your Coordinator class, add the function which will be called in your UIViewRepresentable updateUIView method.
I also suggest when returning a mapView in the makeUIView method in your UIViewRepresentable struct, return the context.coordinator.mapView rather than creating an instance there.
class Coordinator: NSObject, MKMapViewDelegate, CLLocationManagerDelegate {
var mapView = MKMapView()
func performActionFromSwiftUI() {
print("tapped")
}
#objc func addAnnotation(gesture: UIGestureRecognizer) {
if gesture.state == .ended {
if let mapView = gesture.view as? MKMapView {
let point = gesture.location(in: mapView)
let locationCoordinate = mapView.convert(point, toCoordinateFrom: mapView)
let myPin = MKPointAnnotation()
myPin.title = "Latitude: \(locationCoordinate.latitude), Longitude: \(locationCoordinate.longitude)"
myPin.coordinate = locationCoordinate
mapView.addAnnotation(myPin)
}
}
}
}

How to provide an editing interface for a dictionary in SwiftUI?

I have data in a dictionary which is [String: String]. What I want to provide is an interface to the user to edit the values in the dictionary, while the keys remain fixed. I can see how to display the values, but putting them into a TextField is what I want, and haven't been able to find how to do.
Here is the code:
struct dictionaryEditor: View {
#Binding var entries: [String: String]
var body: some View {
List {
ForEach(entries.sorted(by: <), id: \.key) { key, value in
HStack {
Text(key)
TextField("", text: $entries[key])
}
}
}
}
}
This doesn't compile, with no fewer than three errors on the TextField line:
Cannot convert value of type 'Slice<Binding<[String : String]>>' to expected argument type 'Binding'
Cannot convert value of type 'String' to expected argument type 'Range<Binding<[String : String]>.Index>'
Referencing subscript 'subscript(_:)' on 'Binding' requires that '[String : String]' conform to 'MutableCollection'
So obviously I am doing things incorrectly, but I am lost trying to find what the correct way would be, and haven't been able to find an answer in an internet search. Can anyone point me in the right direction?
you could try this simple approach:
struct dictionaryEditor: View {
#Binding var entries: [String: String]
var body: some View {
List {
ForEach(entries.keys.sorted(by: <), id: \.self) { key in
HStack {
Text(key)
TextField("", text: Binding(
get: { entries[key]! },
set: { entries[key] = $0 }
))
}
}
}
}
}
struct ContentView: View {
#State var entries: [String: String] = ["key1":"val1", "key2":"val2", "key3":"val3", "key4":"val4"]
var body: some View {
dictionaryEditor(entries: $entries)
Button(action: { print("----> entries: \(entries)") }) {
Text("print entries")
}
}
}
The problem is that entries[key] returns an optional String value while the text parameter of TextField expects a Binding of non optional String.
You can create an optional binding extension and then you can use it safely:
extension Binding where Value == String? {
var optionalBind: Binding<String> {
.init(
get: {
wrappedValue ?? ""
}, set: {
wrappedValue = $0
}
)
}
}
Then you can just add the optionalBind to your code:
struct dictionaryEditor: View {
#Binding var entries: [String: String]
var body: some View {
List {
ForEach(entries.sorted(by: <), id: \.key) { key, value in
HStack {
Text(key)
TextField("", text: $entries[key].optionalBind) // <--
}
}
}
}
}

Result of 'HymnLyrics' initializer is unused

I have the following error in my code. Please help me why I cannot use my HymnLyrics() struct inside button action. It works for NavigationLink of destination but I don't wanna use it.
import SwiftUI
struct ContentView: View {
#EnvironmentObject var tappingSwitches: TapToggle
let zoLyrics: [Lyric] = LyricList.hymnLa.sorted { lhs, rhs in
return lhs.zoTitle < rhs.zoTitle
}
var body: some View {
ScrollView {
ForEach(zoLyrics, id: \.id) { zoLyric in
VStack {
Button(action: {
HymnLyrics(lyrics: LyricList.hymnLa)// Here is the error, I can use directly by using NavagationLink
self.tappingSwitches.isHymnTapped.toggle()
}, label: {
HStack {
Text(zoLyric.zoTitle)
.foregroundColor(Color("bTextColor"))
.lineLimit(1)
.minimumScaleFactor(0.5)
Spacer()
Text("\(zoLyric.number)")
.foregroundColor(Color("bTextColor"))
}
})
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding([.leading, .bottom, .trailing])
}
}
}
}

How to chain ObservableObject?

I have a Game-object that may hold an image. Whenever an image URL is found for a game a new instance of GameImage-object should be created. It will then fetch the image and populate the UIImage property. When this happens the UI should be updated presenting the image.
class Game: ObservableObject {
#Published var image: GameImage?
}
class GameImage: ObservableObject {
let url: URL
#Published var image: UIImage?
private var cancellable: AnyCancellable?
init(url: URL) {
self.url = url
}
func fetch() {
self.cancellable = URLSession.shared.dataTaskPublisher(for: self.url)
.map { UIImage(data: $0.data) }
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] (image) in
guard let self = self else { return }
self.image = image
print(self.url)
print(self.image)
})
}
func cancel() {
cancellable?.cancel()
}
deinit {
cancel()
}
}
struct ContentView: View {
#StateObject var game = Game()
var body: some View {
VStack {
if let image = game.image?.image {
Image(uiImage: image)
} else {
Text("No image.")
}
}
.onAppear(perform: {
guard let gameImageURL = URL(string: "https://cf.geekdo-images.com/itemrep/img/oVEpcbtyWkJjIjk1peTJo6hI1yk=/fit-in/246x300/pic4884996.jpg") else { return }
game.image = GameImage(url: gameImageURL)
game.image!.fetch()
})
}
}
The problem is. After fetch is done the debug console will show that image contains an UIImage. However the UI does not update to show the image. What am I missing here?
There is much more simpler solution than chaining ObservableObject, just separate dependent part into standalone subview... and all will work automatically.
Here is possible approach. Tested with Xcode 12 / iOS 14.
struct ContentView: View {
#StateObject var game = Game()
var body: some View {
VStack {
if nil != game.image {
GameImageView(vm: game.image!)
}
}
.onAppear(perform: {
guard let gameImageURL = URL(string: "https://cf.geekdo-images.com/itemrep/img/oVEpcbtyWkJjIjk1peTJo6hI1yk=/fit-in/246x300/pic4884996.jpg") else { return }
game.image = GameImage(url: gameImageURL)
game.image!.fetch()
})
}
}
struct GameImageView: View {
#ObservedObject var vm: GameImage
var body: some View {
if let image = vm.image {
Image(uiImage: image)
} else {
Text("No image.")
}
}
}

SwiftUI - Button - How to pass a function request to parent

How can I have a button perform an action which triggers a function in its 'parent' view? I'm trying to refactor my code so that components are as small as possible.
In this case, the button performs a few tasks, but one of them is to run a function:
Button(
action: {
self.setViewBackToNil()
}){
Text("Button")
}
// which triggers a function
func setViewBackToNil(){
self.userData.image = nil
self.isProcessing = false
.... etc
}
Now, if I turn the button into its own view, I can't pass self.setViewBackToNil because it's contained within the struct of the parent.
Is there a way for a component to trigger a function within its parent?
The best examples on closures and how they can be used is found in the official swift documentation.
This is a small example on how to pass a closure to your child view which then calls a function of the parent:
struct ChildView: View {
var function: () -> Void
var body: some View {
Button(action: {
self.function()
}, label: {
Text("Button")
})
}
}
struct ParentView: View {
var body: some View {
ChildView(function: { self.fuctionCalledInPassedClosure() })
}
func fuctionCalledInPassedClosure() {
print("I am the parent")
}
}
I hope this helps!
Pass a function
And here is an example to pass the function:
struct ChildView: View {
var function: () -> Void
var body: some View {
Button(action: {
self.function()
}, label: {
Text("Button")
})
}
}
struct ParentView: View {
var body: some View {
ChildView(function: self.passedFunction)
}
func passedFunction() {
print("I am the parent")
}
}
Pass a function with parameters
struct ChildView: View {
var myFunctionWithParameters: (String, Int) -> Void
var body: some View {
Button(action: {
self.myFunctionWithParameters("parameter", 1)
}, label: {
Text("Button")
})
}
}
struct ParentView: View {
var body: some View {
ChildView(myFunctionWithParameters: self.passedFunction)
}
func passedFunction(myFirstParameter: String, mySecondParameter: Int) {
print("I am the parent")
}
}

Resources