I am developing my first app in SwiftUI and my brain have not wrapped around certain things yet.
I have to display prices, titles and descriptions of inapp purchases on the interface.
I have a singleton model like this, that loads as the app starts.
class Packages:ObservableObject {
enum Package:String, CaseIterable {
typealias RawValue = String
case package1 = "com.example.package1"
case package2 = "com.example.package2"
case package3 = "com.example.package3"
}
struct PurchasedItem {
var productID:String?
var localizedTitle:String = ""
var localizedDescription:String = ""
var localizedPrice:String = ""
}
static let sharedInstance = Packages()
#Published var purchasedItems:[PurchasedItem] = []
func getItem(_ productID:String) -> PurchasedItem? {
return status.filter( {$0.productID == productID } ).first
}
func getItemsAnync() {
// this will fill `purchasedItems` asynchronously
}
purchasedItems array will be filled with PurchasedItems, asynchronous, as the values of price, title and description come from the App Store.
Meanwhile, at another part of the interface, I am displaying buttons on a view, like this:
var buttonPackage1String:String {
let item = Packages.sharedInstance.getItem(Packages.Package.package1.rawValue)!
let string = """
\(umItem.localizedTitle) ( \(umItem.localizedPrice) ) \
\(umItem.localizedDescription) )
"""
return string
}
// inside var Body
Button(action: {
// purchase selected package
}) {
Text(buttonPackage1String)
.padding()
.background(Color.green)
.foregroundColor(.white)
.padding()
}
See my problem?
buttonPackage1String is built with title, description and price from an array called purchasedItems that is stored inside a singleton and that array is filled asynchronously, being initially empty.
So, at the time the view is displayed all values may be not retrieved yet but will, eventually.
Is there a way for the button is updated after the values are retrieved?
Calculable properties are not observed by SwiftUI. We have to introduce explicit state for this and update it once dependent dynamic data changed.
Something like,
#ObservedObject var packages: Packages
...
#State private var title: String = ""
...
Button(action: {
// purchase selected package
}) {
Text(title)
}
.onChange(of: packages.purchasedItems) { _ in
self.title = buttonPackage1String // << here !!
}
Below is a small pseudo code, I'm not sure it's suitable for your project but might give you some hint.
Packages.sharedInstance
.$purchasedItems //add $ to get the `Combine` Subject of variable
.compactMap { $0.firstWhere { $0.id == Packages.Package.package1.rawValue } } //Find the correct item and skip nil value
.map { "\($0.localizedTitle) ( \($0.localizedPrice) ) \ \($0.localizedDescription) )" }
.receive(on: RunLoop.main) // UI updates
.assign(to: \.buttonTitleString, on: self) //buttonTitleString is a #Published variable of your View model
.store(in: &cancellables) //Your cancellable collector
Combine framework knowledge is a must to start with SwiftUI.
Related
Very new to coding and trying to teach myself swift. Running into a problem that I am having trouble understanding. Apologies if my code is a mess or my question is not clear.
I am creating a navigationview from a list I have created. I would like each list item to link to a detail page, that has a button opening a qlpreview of a word document, specific to that list item.
Firstly I have set the structure of my list items:
struct ScriptItem: Identifiable {
let id = UUID()
let ex: String
let phase: String
let doc: String
}
I then have my list and navigationview struct set up to link to a detailed view
struct ScriptsView: View {
#State private var queryString = ""
private let scriptList: [ScriptItem] = [
ScriptItem(ex: "101",
phase: "HMI",
doc: "PILOT NOTES EX 101"),
ScriptItem(ex: "102",
phase: "HMI",
doc: "PILOT NOTES EX 201")].sorted(by: {$0.ex < $1.ex})
var filteredScript: [ScriptItem] {
if queryString.isEmpty {
return scriptList
} else {
return scriptList.filter {
$0.ex.lowercased().contains(queryString.lowercased()) == true || $0.phase.lowercased().contains(queryString.lowercased()) == true
}
}
}
var body: some View {
NavigationView {
List(filteredScript) { scriptItem in
NavigationLink(destination: ScriptDetails(scriptitem: scriptItem)) {
HStack {
ZStack {
Text(scriptItem.ex)
}
Text(scriptItem.phase)
}
}
} .navigationBarTitle("Exercise Scripts")
} .searchable(text: $queryString, placement: .navigationBarDrawer(displayMode: .always), prompt: "Script Search")
}
}
My problem is in the next section - in the detail struct I'm trying to create a url link using forResource: but when I use the string from the previous struct I get the following error:
CANNOT USE INSTANCE MEMBER 'SCRIPTITEM' WITHIN PROPERTY INITIALIZER. PROPERTY INITIALIZERS RUN BEFORE 'SELF' IS AVAILABLE.
struct ScriptDetails: View {
let scriptitem: ScriptItem
let fileUrl = Bundle.main.url(
forResource: scriptitem.doc, withExtension: "docx"
)
ETC ETC ETC
Is there a way around this error that allows me to refer to the string I need from the previous struct?
I have a publisher which returns an array of RetailStoreSlotDay objects. I need to separate these out based on a certain property and then assign to separate publishers within the view model.
So, my publisher is:
#Published var selectedDaySlot: RetailStoreSlotDay?
Within the RetailStoreSlotDay object I have a property called 'daytime' which is set to either:
"morning"
"afternoon"
"evening"
I then have these separate publishers that I need to assign values to when the selectedDaySlot is amended:
#Published var morningTimeSlots = [RetailStoreSlotDayTimeSlot]()
#Published var afternoonTimeSlots = [RetailStoreSlotDayTimeSlot]()
#Published var eveningTimeSlots = [RetailStoreSlotDayTimeSlot]()
At the moment, I have the following subscription set up and declared in the init of the view model:
private func setupDeliveryDaytimeSectionSlots() {
$selectedDaySlot
.map { timeSlot in
return timeSlot?.slots
}
.replaceNil(with: [])
.sink(receiveValue: { slots in
self.morningTimeSlots = slots.filter { $0.daytime == "morning" }
self.afternoonTimeSlots = slots.filter { $0.daytime == "afternoon" }
self.eveningTimeSlots = slots.filter { $0.daytime == "evening" }
})
.store(in: &cancellables)
}
This works fine, but I'm sure there must be an operator which will perform this in a more sophisticated way, whereby I can assign the values without using sink. Wondering if there is a better way around this.
You could group these slots into a dictionary, using Dictionary(grouping:by:):
let dictionaryPublisher = $selectedDaySlot
.map { timeSlot in
Dictionary(grouping: timeSlot?.slots ?? [], by: \.daytime)
}
Then you can assign the values associated with the different keys of the dictionary to the different properties:
dictionaryPublisher.map { $0["morning"] ?? [] }.assign(to: &self.$morningTimeSlots)
dictionaryPublisher.map { $0["afternoon"] ?? [] }.assign(to: &self.$afternoonTimeSlots)
dictionaryPublisher.map { $0["evening"] ?? [] }.assign(to: &self.$eveningTimeSlots)
Rather than using strings as the dayTime values, consider using an enum instead:
enum TimeOfDay: Hashable {
case morning, afternoon, evening
}
I am a novice at programming and exploring SwiftUI. I've been tackling a challenge for too long, and hoping that someone can guide me to the right direction!
I want a list of interlinked sliders (as in Interlinked Multiple Sliders in SwiftUI), but with the number of sliders that change dynamically, depending on actions taken by a user.
For example, a user can choose various items, and later on adjust the percentage variable with sliders (and where these percentages are interdependent as in the linked example).
class Items: ObservableObject {
#Published var components = [ItemComponent]()
func add(component: itemComponent){
components.append(component)
}
}
struct ItemComponent: Hashable, Equatable, Identifiable {
var id = UUID().uuidString
var name: String = ""
var percentage: Double
}
Conceptually, it seems I need to do two things to adapt the linked code:
generate an array of Binding with the number of elements equal to Items.Component.EndIndex and
assign each Binding to the percentage of each ItemComponent.
I am fumbling on both. For 1., I can easily manually create any number of variables, e.g.
#State var value1 = 100
#State var value2 = 100
#State var value3 = 100
let allBindings = [$value1, $value2, $value3]
but how do I generate them automatically?
For 2., I can use ForEach() to call the components, or Index, but not both together:
ForEach(Items.components){ component in
Text("\(component.name)")
Text("\(component.percentage)")
}
ForEach(Items.components.indices){ i in
synchronizedSlider(from: allBindings, index: i+1)
}
In broken code, what I want is something like:
ForEach(Items.component){component in
HStack{
Text("component.name")
Spacer()
synchronizedSlider(from: allBindings[$component.percentage], index: component.indexPosition)
}
where allBindings[$component.percentage] is a binding array comprised of each itemComponent's percentage, and the index is an itemComponent's index.
I am happy to share more code if relevant. Any help would be greatly appreciated!
To adapt the existing code you linked, if you're going to have a dynamic number of sliders, you'll definitely want your #State to be an array, rather than individual #State variables, which would have to be hard coded.
Once you have that, there are some minor syntax issues changing the synchronizedBinding functions to accept Binding<[ItemComponent]> rather than [Binding<Double>], but they are pretty minor. Luckily, the existing code is pretty robust outside of the initial hard-coded states, so there isn't any additional math to do with the calculations.
I'm using ItemComponent rather than just Double because your sample code included it and having a model with a unique id makes the ForEach code I'm using for the sliders easier to deal with, since it expects uniquely-identifiable items.
struct ItemComponent: Hashable, Equatable, Identifiable {
var id = UUID().uuidString
var name: String = ""
var percentage: Double
}
struct Sliders: View {
#State var values : [ItemComponent] = [.init(name: "First", percentage: 100.0),.init(name: "Second", percentage: 0.0),.init(name: "Third", percentage: 0.0),.init(name:"Fourth", percentage: 0.0),]
var body: some View {
VStack {
Button(action: {
// Manually setting the values does not change the values such
// that they sum to 100. Use separate algorithm for this
self.values[0].percentage = 40
self.values[1].percentage = 60
}) {
Text("Test")
}
Button(action: {
self.values.append(ItemComponent(percentage: 0.0))
}) {
Text("Add slider")
}
Divider()
ScrollView {
ForEach(Array(values.enumerated()),id: \.1.id) { (index,value) in
Text(value.name)
Text("\(value.percentage)")
synchronizedSlider(from: $values, index: index)
}
}
}.padding()
}
func synchronizedSlider(from bindings: Binding<[ItemComponent]>, index: Int) -> some View {
return Slider(value: synchronizedBinding(from: bindings, index: index),
in: 0...100)
}
func synchronizedBinding(from bindings: Binding<[ItemComponent]>, index: Int) -> Binding<Double> {
return Binding(get: {
return bindings[index].wrappedValue.percentage
}, set: { newValue in
let sum = bindings.wrappedValue.indices.lazy.filter{ $0 != index }.map{ bindings[$0].wrappedValue.percentage }.reduce(0.0, +)
// Use the 'sum' below if you initially provide values which sum to 100
// and if you do not set the state in code (e.g. click the button)
//let sum = 100.0 - bindings[index].wrappedValue
let remaining = 100.0 - newValue
if sum != 0.0 {
for i in bindings.wrappedValue.indices {
if i != index {
bindings.wrappedValue[i].percentage = bindings.wrappedValue[i].percentage * remaining / sum
}
}
} else {
// handle 0 sum
let newOtherValue = remaining / Double(bindings.wrappedValue.count - 1)
for i in bindings.wrappedValue.indices {
if i != index {
bindings[i].wrappedValue.percentage = newOtherValue
}
}
}
bindings[index].wrappedValue.percentage = newValue
})
}
}
I am trying to populate a picker based on the selection of another picker. I am new to Swift and have been beating my head on this for way too long. I am sure its not as difficult as I am making it but I would appreciate any assistance.
I think my biggest issue is passing the selection of the first picker to the array name of the second. I have used switch case, tried to pass the selection raw value...etc. Below is a sample of what I would like it to look like without the binding of the pickers. Thanks
import SwiftUI
struct veggie: View {
let veggies = ["Beans", "Corn", "Potatoes"]
let beanList = ["Pole", "String", "Black"]
let cornList = ["Peaches & Cream", "Sweet"]
let potatoList = ["Yukon Gold", "Idaho"]
#State private var selectedVeggie = "Bean"
#State private var selectedBean = "Pole"
#State private var selectedType = ""
var body: some View {
NavigationView{
VStack{
Form{
Picker("Please choose a veggie", selection: $selectedVeggie)
{
ForEach(veggies, id: \.self) {
Text($0)
}
}
Text("You selected \(selectedVeggie)")
Picker("Type", selection: $selectedBean)
{
ForEach(beanList, id: \.self) {
Text($0)
}
}
}
} .navigationTitle("Veggie Picker")
}
}
}
I'm imagining that you want this to be somewhat dynamic and not hardcoded with if statements. In order to accomplish that, I setup an enum with the veggie types and then a dictionary that maps the veggie types to their subtypes. Those look like:
enum VeggieType : String, CaseIterable {
case beans, corn, potatoes
}
let subTypes : [VeggieType: [String]] = [.beans: ["Pole", "String", "Black"],
.corn: ["Peaches & Cream", "Sweet"],
.potatoes: ["Yukon Gold", "Idaho"]]
Then, in the view, I did this:
struct ContentView: View {
#State private var selectedVeggie : VeggieType = .beans
#State private var selectedSubtype : String?
var subtypeList : [String] {
subTypes[selectedVeggie] ?? []
}
var body: some View {
NavigationView{
VStack{
Form{
Picker("Please choose a veggie", selection: $selectedVeggie)
{
ForEach(VeggieType.allCases, id: \.self) {
Text($0.rawValue.capitalized)
}
}
Text("You selected \(selectedVeggie.rawValue.capitalized)")
Picker("Type", selection: $selectedSubtype)
{
ForEach(subtypeList, id: \.self) {
Text($0).tag($0 as String?)
}
}
}
} .navigationTitle("Veggie Picker")
}
}
}
The selectedVeggie is now typed with VeggieType. The selectedSubtype is an optional String that doesn't have an initial value (although you could set one if you want).
The first picker goes through all of the cases of VeggieType. The second one dynamically changes based on the computed property subtypeList which grabs the item from the subTypes dictionary I had made earlier.
The tag item is important -- I had to make the tag as String? because SwiftUI wants the selection parameter and the tag() to match exactly.
Note: you say you're new to Swift, so I'll mention that you had named your view `veggie` -- in Swift, normally types have capital names. I named mine `ContentView`, but you could rename it `VeggieView` or something like that.
I'm working on MacOS and I try to make NumberField — something like TextField, but for numbers. In rather big tree of views at the top I had:
...
VStack {
ForEach(instances.indices, id:\.self) {index in
TextField("",
text: Binding(
get: {"\(String(format: "%.1f", instances[index].values[valueIndex]))"},
set: {setValueForInstance(index, valueIndex, $0)})
)
}
}
...
And it worked well, but not nice:
✔︎ when I changed value, all View structure was redrawn – good
✔︎ values was updated if they were changed by another part of Views structure – good
✖︎ it was updated after each keypresses, which was annoying, when I tried to input 1.2, just after pressing 1 view was updated to 1.0. Possible to input every number but inconvenient – bad
So, I tried to build NumberField.
var format = "%.1f"
struct NumberField : View {
#Binding var number: Double {
didSet {
stringNumber = String(format: format, number)
}
}
#State var stringNumber: String
var body: some View {
TextField("" , text: $stringNumber, onCommit: {
print ("Commiting")
if let v = Double(stringNumber) {
number = v
} else {
stringNumber = String(format:format, number)
}
print ("\(stringNumber) , \(number)" )
})
}
init (number: Binding<Double>) {
self._number = number
self._stringNumber = State(wrappedValue: String(format:format, number.wrappedValue))
}
}
And It's called from the same place as before:
...
VStack {
ForEach(instances.indices, id:\.self) {index in
NumberField($instances[index].values[valueIndex])
}
}
...
But in this case it never updates NumberField View if values was changed by another part of View. Whats's wrong? Where is a trick?