In the answers to my question on How to add a generic SwiftUI view to another view, I learned that this can be done using #ViewBuilder.
While this works fine for most of my use cases, I now came across a new problem:
The #ViewBuilder solution basically creates the ContentView view outside the GenericView<Content: View>
The ContentView is than passed to the GenericView<Content: View> which shows it
BUT: What if the ContentView has to be created inside GenericView<Content: View> because it requires some parameters which are only available there?
Example:
UserView is created by providing a user ID
UserViews view model fetches the user name using the ID. So the which creates the UserView does only know the ID but not the name. The name is only available within UserView
UserView is used at different places within an app or even in different apps. The different places require to show the username in different layouts/styles/etc. To not hard code all layouts into UserView, the view is generic and is given a Content view type which is used to show the username
Code
protocol NameView: View {
init(name: String)
}
struct NameOnlyView: NameView {
private let name: String
init(name: String) {
self.name = name
}
var body: some View {
Text(name)
}
}
struct NameGreetingView: NameView {
private let name: String
init(name: String) {
self.name = name
}
var body: some View {
Text("Hello \(name)")
}
}
struct UserView<Content: NameView>: View {
private let name: String
private let nameView: Content
init(userId: Int, #ViewBuilder nameViewBuilder: (String) -> Content) {
name = LoadUserName(usingId: userId)
nameView = nameViewBuilder(name)
}
var body: some View {
nameView
}
}
struct SomeView: View {
var body: some View {
// What I would like to do
UserView(userId: 123, NameOnlyView)
UserView(userId: 987, NameGreetingView)
// What is required instead
UserView(userId: 123) {
NameOnlyView("Name which is not known here")
}
}
}
Of course I could move the logic to load the username from the given ID and make it available in SomeView. However, this is just an example for any value which is only available in UserView but not in SomeView. Especially when using the UserView in different apps I do not want to implement the same logic to load the username (or whatever) in all possible parent views of UserView.
Can this be solved using the #ViewBuilder solution?
Can this be solved in SwiftUI at all?
Or am I completely on the wrong track?
Take a look at SwiftUI's styling system, e.g. ButtonStyle, and the blog post Encapsulating SwiftUI view styles (Swift by Sundell) has some great info. Hopefully you could end up with something like:
List {
UserView(userId: 987)
UserView(userId: 123)
}
.userViewStyle(.nameOnly)
Related
I' trying to create a set of Toggles, that need to be stored in one core data field of type "Transformable". I started with this example:
https://developer.apple.com/forums/thread/118595
in combination with other ideas from stack.
I'm trying to get this way:
Create a Set of structs like this
struct AllScopes: Identifiable, Hashable {
var id: UUID
var name: String
var notify: Bool
}
[...]
// all the stuff with View and body with
#State var scopes = Set<AllScopes>()
[...]
// and here I run through my FetchRequest to fill the Set
.onAppear {
for scope in allScopes {
scopes.insert(
AllScopes(
id: scope.id!,
name: scope.name!,
notify: false
)
)
}
}
In the end I've got a nice Set with all my scopes.
I call a new View with YearlyReportPage6(scopes: $scopes)
And now my problem - the next view:
struct YearlyReportPage6: View {
#Binding var scopes: Set<AllScopes>
init(scopes: Binding<Set<AllScopes>>) {
_scopes = scopes
}
var body: some View {
VStack {
ForEach(scopes.indices) { index in
Toggle(isOn: self.$scopes[index].notify) {
Text(self.scopes[index].name)
}
}
}
}
}
But all in ForEach creates errors. Either Binding in isOn: is wrong, or ForEach can't work with the set, or the Text is not a String, or, or, or...
In the end there should be a list of Toggles (checkboxes) and the selection should be stored in database.
Changing the Set into a simple Array like #State var scopes = [AllScopes]() will work for the Toggles, but how can I store this into a Transformable?
ForEach(Array(scopes).indices) { index in
Toggle(isOn: self.$scopes[index].notify) {
Text(self.scopes[index].name)
}
}
To summarize:
either how can I create the list of Toggles with the Set of AllScopes
or how can I store the Array / Dictionary into the Transformable field?
I hope, you can understand my clumsy English. :-)
Okay so I've been working on this for several days now and have not had any luck with an answer that makes any sense. I have a form in SwiftUI, using #ObservedObject to pull variables from a struct. In that form, I have a variety of text fields and pickers that the user can interact with. HOWEVER, I cannot figure out how to get my "Add" button to actually add that data to any of the other views in the app. I followed the sandwiches tutorial from WWDC20, with significant changes, so there is a swift file with "testData" and essentially I'm trying to get it so that the button uses the user input to append the testData and show that instead of nothing.
struct Horse: Identifiable {
var id = UUID()
var name: String
var gender: String
var breed: String
var type: String
var scale: String
var brand: String
var finish: String
var specialty: String
var imageName: String { return name }
var thumbnailName: String { return name + "Thumb" }
}
let testData = [
Horse(name: "Van Gogh", gender: "Stallion", breed: "Unknown", type: "Customized", scale: "Stablemate", brand: "Peter Stone", finish: "Gloss", specialty: "OOAK")
]
So this is what I'm using to establish testData and the parameters for what should be included in it.
func addANewHorse() {
withAnimation {
testStore.horses.append(Horse(name: "\(horseDetails.horseName)", gender: "\(horseDetails.selectedGender.rawValue)", breed: "\(horseDetails.horseBreed)", type: "\(horseDetails.type.rawValue)", scale: "\(horseDetails.scale.rawValue)", brand: "\(horseDetails.brand.rawValue)", finish: "\(horseDetails.finish.rawValue)", specialty: "\(horseDetails.specialty.rawValue)"))
}
}
Button("Add", action: {
addANewHorse();
self.presentationMode.wrappedValue.dismiss()
})
And that is what I'm using to try and append the testData to update with the users input. I know this is kind of choppy but does anyone have any advice whatsoever?
---EDIT---
My main app file looks like this...
#main
struct Pferd_HerdApp: App {
#StateObject private var store = HorseStore()
#StateObject private var horseDetails = HorseDetails()
var body: some Scene {
WindowGroup {
ContentView(store: store, horseDetails: HorseDetails())
}
}
}
my horse store class looks like this...
class HorseStore: ObservableObject {
#Published var horses: [Horse]
init(horses: [Horse] = []) {
self.horses = horses
}
}
let testStore = HorseStore(horses: testData)
Also, "HorseDetails" is the observableobject I'm trying to pull data from to append the testData, so here is the code for that
class HorseDetails: ObservableObject {
#Published var horseName = ""
#Published var selectedGender = Gender.allCases[0]
#Published var horseBreed = ""
#Published var purchaseDate = Date()
#Published var winCount = ""
#Published var notes = ""
#Published var brand = Brands.allCases[0]
#Published var type = Type.allCases[0]
#Published var scale = Scale.allCases[0]
#Published var finish = Finish.allCases[0]
#Published var specialRun = false
#Published var specialty = Specialty.allCases[0]
}
var horseDetails = HorseDetails()
and I changed the let for testData to a variable
Since your Question leaves a lot of code out, I will be making a few assumptions. I'm assuming that your form (where you have the button to add data) and your view for displaying the data are in two different views. You have not included your view model in the code, although there was an instance of your view model (testStore) used in the code above. You need to make sure that somewhere at the root of your view hierarchy, you made an instance of your view model (I'm assuming its called TestStoreViewModel) and passed that as an environment object to your subviews. For example, you should something like this
#main
struct YourApp: App {
let testStoreViewModel = TestStoreViewModel()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(testStoreViewModel)
}
}
}
in all of your views where you need to use the data from your TestStoreViewModel, you should declare it like so
#EnvironmentObject var testStore:TestStoreViewModel
Using environment objects means that your observable object is automatically synced across all of your views that use the environment object. Everything else in the code above should work fine with the use of EnvironmentObjects and a single source of truth. For more on environment objects, you can check out this article which in my opinion is great at explaining Environment Objects in swiftui. It is also important to note that in that article, it mentioned the use of a SceneDelegte and the ContentView being wrapped around a UIHostingController. That was replaced by the first block of code I showed you above.
For the new delegate file in iOS 14 I need to include both the .environmentObject settings and the UserSettings: ObservableObject (which is a Realm Class).
But I first need to create the User data if there is none (first time user) otherwise it give me a null error and crashes.
Where would I put the code to initiate the user before calling it in the body loads?
#main
struct myapp_App: App {
let userSettings = UserSettings() // calling the data which will not exist if initial user
var body: some Scene {
WindowGroup {
ContentView().environmentObject(userSettings)
}
}
}
Thank you.
I put the code to create the user if first time inside the int() of the ObservableObject.
I hope can only hope this code is proper? But it works.
Good luck.
class UserSettings: ObservableObject {
#Published var name: String? = nil
#Published var email: String? = nil
init(){
if User.userExist() == false {
User.initiateUser()
}
let u = User.getUser()
name = u!.name
email = u!.email
}
}
Can anyone share how we can implement AVSpeechSynthesizerDelegate in SwiftUI.
how we can listen to delegate callbacks methods in SwiftUI app.
Thanks
One solution would be to define a class which conforms to ObservableObject. The idea would be to use an #Published property to enable SwiftUI to make updates to your UI. Here's an example of a simple way to keep track of the state of an AVSpeechSynthesizer (I'm unsure of your actual use case):
final class Speaker: NSObject, ObservableObject {
#Published var state: State = .inactive
enum State: String {
case inactive, speaking, paused
}
override init() {
super.init()
synth.delegate = self
}
func speak(words: String) {
synth.speak(.init(string: words))
}
private let synth: AVSpeechSynthesizer = .init()
}
Then, make this class conform to AVSpeechSynthesizerDelegate like so:
extension Speaker: AVSpeechSynthesizerDelegate {
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) {
self.state = .speaking
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didPause utterance: AVSpeechUtterance) {
self.state = .paused
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
self.state = .inactive
}
// etc...
}
Here, I've simply used the delegate callbacks to update a single #Published property, but you could update however you like here depending on your use case. The main point to bear in mind with ObservableObjects is using the #Published property wrapper for any properties you wish to drive UI updates upon a change in value. Here's an example view:
struct MyView: View {
#ObservedObject var speaker: Speaker
var body: some View {
// 1
Text("State = \(speaker.state.rawValue)")
.onReceive(speaker.$state) { state in
// 2
}
}
}
Note how there's two ways to use #Published properties in SwiftUI Views. 1: Simply read the value. SwiftUI will update your view upon a value change. 2: Access the #Published property's publisher with the $ prefix. Using Views onReceive method, you can execute code whenever the publisher emits a value.
Multiline text input is currently not natively supported in SwiftUI (hopefully this feature is added soon!) so I've been trying to use the combine framework to implement a UITextView from UIKit which does support multiline input, however i've been having mixed results.
This is the code i've created to make the Text view:
struct MultilineTextView: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.isScrollEnabled = true
view.isEditable = true
view.isUserInteractionEnabled = true
view.backgroundColor = UIColor.white
view.textColor = UIColor.black
view.font = UIFont.systemFont(ofSize: 17)
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
func frame(numLines: CGFloat) -> some View {
let height = UIFont.systemFont(ofSize: 17).lineHeight * numLines
return self.frame(height: height)
}
func makeCoordinator() -> MultilineTextView.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: MultilineTextView
init(_ parent: MultilineTextView) {
self.parent = parent
}
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}
}
}
I've then implemented it in a swiftUI view like:
MultilineTextView(text: title ? $currentItem.titleEnglish : $currentItem.pairArray[currentPair].english)//.frame(numLines: 4)
And bound it to a state variable:
#State var currentItem:Item
It sort of works. However, the state var currentItem:Item contains an array of strings which I'm then cycling through using buttons which update the string array based on what has been inputted into MultilineTextView. This is where i'm encountering a problem where the MultilineTextView seems to bind to only the first string item in the array, and then it won't change. When I use swiftUI's native TextField view this functionality works fine and I can cycle through the string array and update it by inputting text into the TextField.
I think I must be missing something in the MultilineTextView struct to allow this functionality. Any pointers are gratefully received.
Update: Added model structs
struct Item: Identifiable, Codable {
let id = UUID()
var completed = false
var pairArray:[TextPair]
}
struct TextPair: Identifiable, Codable {
let id = UUID()
var textOne:String
var textTwo:String
}
Edit:
So I've done some more digging and I've found what I think is the problem. When the textViewDidChange of the UITextView is triggered, it does send the updated text which I can see in the console. The strange thing is that the updateUIView function then also gets triggered and it updates the UITextView's text with what was in the binding var before the update was sent via textViewDidChange. The result is that the UITextview just refuses to change when you type into it. The strange thing is that it works for the first String in the array, but when the item is changed it won't work anymore.
It appears that SwiftUI creates two variants of UIViewRepresentable, for each binding, but does not switch them when state, ie title is switched... probably due to defect, worth submitting to Apple.
I've found worked workaround (tested with Xcode 11.2 / iOS 13.2), use instead explicitly different views as below
if title {
MultilineTextView(text: $currentItem.titleEnglish)
} else {
MultilineTextView(text: $currentItem.pairArray[currentPair].textOne)
}
So I figured out the problem in the end, the reason why it wasn't updating was because I was passing in a string which was located with TWO state variables. You can see that in the following line, currentItem is one state variable, but currentPair is another state variable that provides an index number to locate a string. The latter was not being updated because it wasn't also being passed into the multiline text view via a binding.
MultilineTextView(text: title ? $currentItem.titleEnglish : $currentItem.pairArray[currentPair].english)
I thought initially that passing in one would be fine and the parent view would handle the other one but this turns out not to be the case. I solved my problem by making two binding variables so I could locate the string that I wanted in a dynamic way. Sounds stupid now but I couldn't see it at the time.