SwiftUI - Passing Dynamic Data via Button - state

I'm trying to pass dynamic details from a master page to a detail page via a Button action. The view I'm passing to, I'm animating from off-screen, into the view using animation. I was able to get this to work when I was passing simple hard coded data, but I'm having an issue trying to figure out how to get it to pass data specific to the item row that was tapped.
I've wrapped the item row element with the button, but not sure I'm passing the right info into the button action parameter.
import SwiftUI
struct SearchView: View {
#State var show = false
var course = courseData
var body: some View {
ZStack {
ScrollView (.vertical, showsIndicators: false) {
VStack(spacing: 0.0) {
ForEach(course) { item in
Button(action: { self.show.toggle() }) {
CourseResultTile(perks: item.perks, hotdeals: item.hotdeals, image: item.image, courseName: item.courseName, distance: item.distance, reviews: item.reviews, priceSpan: item.priceSpan, timeSpan: item.timeSpan)
}
}
}
}.offset(y: 120)
// View I'm animating in below //
FacilityView(show: $show, perks: self.perks, image: self.image, courseName: self.courseName, address: self.address)
.rotation3DEffect(Angle(degrees: show ? 0 : 60), axis: (x: 0, y: 10.0, z: 0))
.animation(.default)
.offset(x: show ? 0 : -UIScreen.main.bounds.width)
.onTapGesture {
self.show.toggle()
}
}
.navigationBarTitle(Text("Search"), displayMode: .inline)
.navigationBarHidden(true)
.edgesIgnoringSafeArea(.top)
}
}
// Course Data Model //
struct Course : Identifiable {
var id = UUID()
var hotdeals: String
var perks: String
var image: String
var courseName: String
var distance: String
var reviews: String
var priceSpan: String
var timeSpan: String
var address: String
}
let courseData = [
Course(hotdeals: "Badge_HotDeals", perks: "Badge_NoPerks", image: "Image_CourseBackground", courseName: "Timacuan Golf Club", distance: "16.8 miles", reviews: "(562)", priceSpan: "$12.00 - $34.00", timeSpan: "1:53PM - 6:00 PM", address: "550 Timacuan Boulevard, Lake Mary, Florida, 32746"),
Course(hotdeals: "Badge_HotDeals", perks: "Badge_NoPerks", image: "Image_Course1", courseName: "Sanctuary Ridge Golf Club", distance: "20.1 miles", reviews: "(895)", priceSpan: "$12.00 - $34.95", timeSpan: "8:46 AM - 5:02 PM", address: "2601 Diamond Club Drive, Clermont, Florida, 34711"),
Course(hotdeals: "Badge_HotDeals", perks: "Badge_NoPerks", image: "Image_Course2", courseName: "Eagle Creek Golf Club - FL", distance: "14.3 miles", reviews: "(1K)", priceSpan: "$12.00 - $42.00", timeSpan: "8:42 AM - 5:51 PM", address: "10350 Emerson Lake Blvd, Orlando, Florida, 32832")
// FacilityView excerpt //
import SwiftUI
struct FacilityView: View {
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter
}
var perks = "Badge_NoPerks"
var image = "Image_Course6"
var courseName = "Panther Lake: Orange County National"
var address = "16301 Phil Ritson Way, Winter Garden, FL 34787"
var holes = "18"
var par = "72"
var length = "6836"
var slope = "127"
#Binding var show: Bool
#State private var todaysDate = Date()
The error I'm getting is "Value of type 'SearchView' has no member 'variable I'm trying to pass'

Related

how to convert muliple int number to string in qr code generator with swiftui

I have made a qr code generator with date and I want to add another picker for hours, and I have tried to conver Int to String, there isn't any problem, when I tried to convert multiple Int to string and it doesn't work and also i want to change the Int format to something like this 12 02:11, first is integer space and time. how could I do that?
My Code:
struct GenerateQRCode: View {
#Binding var time: Date
#Binding var hours: Int
let hour = ["3","6","9","12"]
let filter = CIFilter.qrCodeGenerator()
let cont = CIContext()
var dateFormatter: DateFormatter {
let df = DateFormatter()
df.dateFormat = "HH:mm"
return df
}
var body: some View {
NavigationView{
Image(uiImage: imageGenerate(times:time, hours: hours))
.interpolation(.none)
.resizable()
.frame(width: 150, height: 150, alignment: .center)
}.navigationBarBackButtonHidden(true)
}
func imageGenerate(hours: Int, times: Date)-> UIImage { //<--here how to add integer parameter?
let str = dateFormatter.string(from: start)
let ts = String(hours)
let com = ts + str
let data = com.data(using: .utf8)
filter.setValue(data, forKey: "inputMessage") //
if let qr = filter.outputImage {
if let qrImage = cont.createCGImage(qr, from: qr.extent){
return UIImage(cgImage: qrImage)
}
}
return UIImage(systemName: "xmark") ?? UIImage()
}
}
Preview:
import Foundation
import SwiftUI
import CoreImage.CIFilterBuiltins
struct DatePicker: View {
#State var Time = Date()
#State var sHours = Int()
#State var navigated = false
let hour = ["3", "6", "9", "12"]
var body: some View {
NavigationView{
VStack{
Section{
Text("Please Select Time")
DatePicker("", selection: $startTime, displayedComponents: [.hourAndMinute])
.datePickerStyle(.wheel)
}
Section{
Text("Please Select Minutes")
Picker(selection: $sMinutes, label: Text("Please Select Minutes"))
{
ForEach(0 ..< minutes.count) {
index in Text(self.minutes[index]).tag(index)
}
}
}
Section
{
NavigationLink(destination: GenerateQRCode(start: $Time, minutes: $sHours), isActive: self.$navigated)
{
Text("Complete")
}
}.padding(100)
}.navigationBarTitle("Visitor")
}
}
}
struct DatePicker_Previews: PreviewProvider {
static var previews: some View {
DatePicker()
}
}
Output: "012:30"
what i expected output, should be like this: "3 12:30" first should be hours and then time. i don't know why it only shows 0 if my picker turns to 3. How can i solve it out?

SwiftUI - showing Annotation with dynamic colors on Map with MapKit

hopefully someone can help me with my problem. I have an app where I show a Map with the current location of the user. Then I can tap on a button and it shows all gas stations around the location based on a certain radius. The stations are showed by little map pins. Now I want that the cheapest station has another color than the rest (yellow instead of red). The function for this is already written, but the problem is, that sometimes there is no map pin yellow or it is the wrong one which is yellow. The first tap on the button after the app starts is always good, but the following can be sporadic right or wrong. Here is my code.
Part of my MapView:
#ObservedObject var locationManager = LocationManager.shared
#EnvironmentObject var dataViewModel:DataViewModel
#EnvironmentObject var carViewModel:CarViewModel
#State private var radius: String = ""
#State private var showInput: Bool = false
var body: some View {
ZStack {
Map(coordinateRegion: $locationManager.region, interactionModes: .all, showsUserLocation: true, annotationItems: dataViewModel.annotations, annotationContent: { station in
MapAnnotation(coordinate: station.coordinate) {
if dataViewModel.annotations.count > 0 {
MapAnnotationView(dataViewModel: dataViewModel, station: station)
.onTapGesture {
dataViewModel.currentAnnotation = station
dataViewModel.showStationSheet = true
}
}
}
} )
.accentColor(Color.blue)
.ignoresSafeArea()
This is the function where I get my data from:
guard let url = URL(string: "https://creativecommons.tankerkoenig.de/json/list.php?lat=\(latitude)&lng=\(longitude)&rad=\(radius)&sort=dist&type=all&apikey=\(apiKey)") else { return }
URLSession.shared.dataTask(with: url) { data, response, error in
if error == nil {
if let data = data {
do {
let decodedResponse = try JSONDecoder().decode(HeadData.self, from: data)
DispatchQueue.main.async {
self.gasData.append(decodedResponse)
self.bestAnnotations = [Annotation]()
for bestStation in self.gasData[0].stations {
var id = UUID()
self.annoIDs.append(id)
var tempAnnotation = Annotation(id: id, name: bestStation.name, brand: bestStation.brand, street: bestStation.street, houseNumber: bestStation.houseNumber, postCode: bestStation.postCode, place: bestStation.place, distance: bestStation.dist, diesel: bestStation.diesel ?? 9.999, e5: bestStation.e5 ?? 9.999, e10: bestStation.e10 ?? 9.999, isOpen: bestStation.isOpen, address: bestStation.street, coordinate: CLLocationCoordinate2D(latitude: bestStation.lat, longitude: bestStation.lng))
self.bestAnnotations.append(tempAnnotation)
}
self.calculateBestAnnotation(activeCar: activeCar)
var i = 0
for station in self.gasData[0].stations {
var tempAnnotation = Annotation(id: self.annoIDs[i], name: station.name, brand: station.brand, street: station.street, houseNumber: station.houseNumber, postCode: station.postCode, place: station.place, distance: station.dist, diesel: station.diesel ?? 9.999, e5: station.e5 ?? 9.999, e10: station.e10 ?? 9.999, isOpen: station.isOpen, address: station.street, coordinate: CLLocationCoordinate2D(latitude: station.lat, longitude: station.lng))
i += 1
self.copiedAnnotations.append(tempAnnotation)
}
self.annotations = self.copiedAnnotations
}
} catch let jsonError as NSError {
DispatchQueue.main.async {
self.searchToastError = "Es konnten keine Daten gefunden werden."
self.presentSearchToast = true
}
}
return
}
}
}
.resume()
At first I am saving the decoded json response into an array and I calculate the cheapest gas station so that there is one element left in the array bestAnnotations. After that I append the data to the Annotation Array which is the data source of the Annotations on the MapView.
And then my MapAnnotationView looks like this:
#ObservedObject var locationManager = LocationManager.shared
#ObservedObject var dataViewModel:DataViewModel
#State var station: Annotation
var body: some View {
ZStack {
Circle()
.frame(width: 35, height: 35)
.foregroundColor(station.id == dataViewModel.bestAnnotations[0].id ? .yellow : .red)
Image(systemName: "mappin")
.resizable()
.scaledToFit()
.foregroundColor(.white)
.frame(width: 25, height: 25)
}
}
Hopefully someone can help me with the problem. Maybe there is there something wring with the Dispatch function?

Picker selection not updating

I'm trying to select a default account number from a list of available accounts. The data is from an FMDB data selection. I've created a test view with two types of pickers in it. One that lists the accounts I've retrieved into an array of data records which is a swift struct. The other picker is one that comes from an on-line example to select colors. The colors "selection" bound value updates as expected, but the "selection" bound value that I set does not change when one of two accounts is presented and selected. Below is the code from my test view that compiles and runs. When I select either account value which is [12345678] or [12345679] which appear as two rows in the picker the selection binding value doesn't change. But for the colors selection value it updates. I'm pretty confused here...
The struct for the accounts record is:
// Account record for FMDB
struct AccountRecord: Hashable {
var account_id: Int!
var account_code: Int!
var account_name: String!
var running_balance: Double!
var hidden_balance: Double!
var actual_balance: Double!
}
import SwiftUI
struct PickerTestView: View {
#State private var selectedAccount = 0
#State private var selectedColor = 0
var acctRecords: [Accounts.AccountRecord] {
return Accounts.shared.selectAllAccounts()
}
var colors = ["Red", "Green", "Blue", "Tartan"]
var body: some View {
VStack{
Picker(selection: $selectedAccount, label: Text(""))
{
ForEach (self.acctRecords, id: \.self) { acct in
Text("\(acct.account_code!)")
}
}
Text("selectedAccount = \(selectedAccount)")
.font(.largeTitle)
Picker(selection: $selectedColor, label: Text("Please choose a color")) {
ForEach(0 ..< colors.count) {
Text(self.colors[$0])
}
}
Text("Selectedcolor = \(selectedColor)")
Text("You selected \(colors[selectedColor])")
}
}
}
struct PickerTestView_Previews: PreviewProvider {
static var previews: some View {
PickerTestView()
}
}
Two things are happening:
You need a .tag() on your Picker elements to tell the system which element belongs to what item:
Picker(selection: $selectedAccount, label: Text(""))
{
ForEach (self.acctRecords, id: \.self) { acct in
Text("\(acct.account_code!)").tag(acct.account_id)
}
}
SwiftUI needs the types of the selection parameter and the tag type to be the same. Because in your model, account_id is defined as Int! and not Int, your selectedAccount needs to be Int! as well:
#State private var selectedAccount : Int! = 0
The following works with some test data embedded in:
struct PickerTestView: View {
#State private var selectedAccount : Int! = 1
#State private var selectedColor = 0
var acctRecords: [AccountRecord] {
return [.init(account_id: 1, account_code: 1, account_name: "1", running_balance: 0, hidden_balance: 0, actual_balance: 0),
.init(account_id: 2, account_code: 2, account_name: "2", running_balance: 0, hidden_balance: 0, actual_balance: 0),
.init(account_id: 3, account_code: 3, account_name: "3", running_balance: 0, hidden_balance: 0, actual_balance: 0)
]
}
var colors = ["Red", "Green", "Blue", "Tartan"]
var body: some View {
VStack{
Picker(selection: $selectedAccount, label: Text(""))
{
ForEach (self.acctRecords, id: \.self) { acct in
Text("\(acct.account_code!)").tag(acct.account_id)
}
}
Text("selectedAccount = \(selectedAccount)")
.font(.largeTitle)
}
}
}
Try to add .onChange:
Picker(selection: $selectedAccount, label: Text("")) {
ForEach (self.acctRecords, id: \.self) { acct in
Text("\(acct.account_code!)").tag(acct.account_code) // <- add tag here
}
}
.onChange(of: selectedAccount) {
selectedAccount = $0
}

How to bind a function in view model to a custom view in swiftui?

I have a custom textfield:
struct InputField: View {
var inputText: Binding<String>
var title: String
var placeholder: String
#State var hasError = false
var body: some View {
VStack(spacing: 5.0) {
HStack {
Text(title)
.font(.subheadline)
.fontWeight(.semibold)
Spacer()
}
TextField(placeholder, text: inputText).frame(height: 50).background(Color.white)
.cornerRadius(5.0)
.border(hasError ? Color.red : Color.clear, width: 1)
}
}
}
my view model is:
class LoginViewModel: ObservableObject {
#Published var username = "" {
didSet {
print("username is: \(username)")
}
}
func checkUsernameisValid() -> Bool {
return username.count < 6
}
}
and my final login view:
#ObservedObject var loginViewModel = LoginViewModel()
var inputFields: some View {
VStack {
InputField(inputText: $loginViewModel.username, title: "Username:", placeholder: " Enter your username", hasError: $loginViewModel.checkUsernameisValid())
InputField(inputText: $loginViewModel.password, title: "Password:", placeholder: " Enter your password", hasError: $loginViewModel.checkUsernameisValid())
}
}
Now this complains at hasError:$loginViewModel.checkUsernameisValid() that I cannot bind a function to the state var hasError.
How can I make this work by still using the function checkUsernameisValid() to update my custom textfield view ?
One way I can solve this is by using another published var in my view model
#Published var validUsername = false
func checkUsernameisValid() {
validUsername = username.count < 6
}
and keep calling this function in the didSet of my username var
#Published var username = "" {
didSet {
print("username is: \(username)")
checkUsernameisValid()
}
}
finally use the new published var to bind the hasError:
hasError: $loginViewModel.validUsername
My question is, is this the only way ? i.e use #published var for binding, and I cannot use standalone functions directly to do the same thing instead of using more and more #Published variables ?
You don't need binding for error. The InputField will be updated by inputText, so you just need a regular property, like
struct InputField: View {
var inputText: Binding<String>
var title: String
var placeholder: String
var hasError = false // << here !!
// ...
}
and now pass just call
InputField(inputText: $loginViewModel.username, title: "Username:", placeholder: " Enter your username",
hasError: loginViewModel.checkUsernameisValid()) // << here !!
Tested with Xcode 12.1 / iOS 14.1
Try:
#ObservedObject var loginViewModel = LoginViewModel()
var inputFields: some View {
VStack {
InputField(inputText: $loginViewModel.username, title: "Username:", placeholder: " Enter your username", hasError: loginViewModel.checkUsernameisValid())
InputField(inputText: $loginViewModel.password, title: "Password:", placeholder: " Enter your password", hasError: loginViewModel.checkUsernameisValid())
}
}
The function works on the actual value on the bound variable, not the binding itself.

SwiftUI page control implementation

I need to implement something like an animated page control. And I don't want to use integration with UIKit if possible. I have pages array containing 4 views I need to switch between. I create the animation itself by changing the value of progress variable using timer. And I have the following code right now
#State var pages: [PageView]
#State var currentIndex = 0
#State var nextIndex = 1
#State var progress: Double = 0
var body: some View {
ZStack {
Button(action: {
self.isAnimating = true
}) { shape.onReceive(timer) { _ in
if !self.isAnimating {
return
}
self.refreshAnimatingViews()
}
}.offset(y: 300)
pages[currentIndex]
.offset(x: -CGFloat(pow(2, self.progress)))
pages[nextIndex]
.offset(x: CGFloat(pow(2, (limit - progress))))
}
}
It is animating great - current page is moved to the left until it disappears, and the next page is revealed from the right taking its place. At the end of animation I add 1 to both indices and reset progress to 0. But once the animation (well not exactly an animation - I just change the value of progress using timer, and generate every state manually) is over, the page with index 1 is swapped back to page with index 0. If I check with debugger, currentIndex and nextIndex values are correct - 1 and 2, but the page displayed after animation is always the one I started with (with index 0). Does anybody know why this is happening?
The whole code follows
struct ContentView : View {
let limit: Double = 15
let step: Double = 0.3
let timer = Timer.publish(every: 0.01, on: .current, in: .common).autoconnect()
#State private var shape = AnyView(Circle().foregroundColor(.blue).frame(width: 60.0, height: 60.0, alignment: .center))
#State var pages: [PageView]
#State var currentIndex = 0
#State var nextIndex = 1
#State var progress: Double = 0
#State var isAnimating = false
var body: some View {
ZStack {
Button(action: {
self.isAnimating = true
}) { shape.onReceive(timer) { _ in
if !self.isAnimating {
return
}
self.refreshAnimatingViews()
}
}.offset(y: 300)
pages[currentIndex]
.offset(x: -CGFloat(pow(2, self.progress)))
pages[nextIndex]
.offset(x: CGFloat(pow(2, (limit - progress))))
}.edgesIgnoringSafeArea(.vertical)
}
func refreshAnimatingViews() {
progress += step
if progress > 2*limit {
isAnimating = false
progress = 0
currentIndex = nextIndex
if nextIndex + 1 < pages.count {
nextIndex += 1
} else {
nextIndex = 0
}
}
}
}
struct PageView: View {
#State var title: String
#State var imageName: String
#State var content: String
let imageWidth: Length = 150
var body: some View {
VStack(alignment: .center, spacing: 15) {
Text(title).font(Font.system(size: 40)).fontWeight(.bold).lineLimit(nil)
Image(imageName)
.resizable()
.frame(width: imageWidth, height: imageWidth)
.cornerRadius(imageWidth/2)
.clipped()
Text(content).font(.body).lineLimit(nil)
}.padding(60)
}
}
struct MockData {
static let title = "Eating grapes 101"
static let contentStrings = [
"Step 1. Break off a branch holding a few grapes and lay it on your plate.",
"Step 2. Put a grape in your mouth whole.",
"Step 3. Deposit the seeds into your thumb and first two fingers.",
"Step 4. Place the seeds on your plate."
]
static let imageNames = [
"screen 1",
"screen 2",
"screen 3",
"screen 4"
]
}
in SceneDelegate:
if let windowScene = scene as? UIWindowScene {
let pages = (0...3).map { i in
PageView(title: MockData.title, imageName: MockData.imageNames[i], content: MockData.contentStrings[i])
}
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView(pages:
pages))
self.window = window
window.makeKeyAndVisible()
}
The following solution works. I think the problem was switching out views while SwiftUI tries to diff and update them is not something SwiftUI is good at.
So just use the same two PageView views and swap out their content based on the current index.
import Foundation
import SwiftUI
import Combine
struct PagesView : View {
let limit: Double = 15
let step: Double = 0.3
#State var pages: [Page] = (0...3).map { i in
Page(title: MockData.title, imageName: MockData.imageNames[i], content: MockData.contentStrings[i])
}
#State var currentIndex = 0
#State var nextIndex = 1
#State var progress: Double = 0
#State var isAnimating = false
static let timerSpeed: Double = 0.01
#State var timer = Timer.publish(every: timerSpeed, on: .current, in: .common).autoconnect()
#State private var shape = AnyView(Circle().foregroundColor(.blue).frame(width: 60.0, height: 60.0, alignment: .center))
var body: some View {
ZStack {
Button(action: {
self.isAnimating.toggle()
self.timer = Timer.publish(every: Self.timerSpeed, on: .current, in: .common).autoconnect()
}) { self.shape
}.offset(y: 300)
PageView(page: pages[currentIndex])
.offset(x: -CGFloat(pow(2, self.progress)))
PageView(page: pages[nextIndex])
.offset(x: CGFloat(pow(2, (self.limit - self.progress))))
}.edgesIgnoringSafeArea(.vertical)
.onReceive(self.timer) { _ in
if !self.isAnimating {
return
}
self.refreshAnimatingViews()
}
}
func refreshAnimatingViews() {
progress += step
if progress > 2*limit {
isAnimating = false
progress = 0
currentIndex = nextIndex
if nextIndex + 1 < pages.count {
nextIndex += 1
} else {
nextIndex = 0
}
}
}
}
struct Page {
var title: String
var imageName: String
var content: String
let imageWidth: CGFloat = 150
}
struct PageView: View {
var page: Page
var body: some View {
VStack(alignment: .center, spacing: 15) {
Text(page.title).font(Font.system(size: 40)).fontWeight(.bold).lineLimit(nil)
Image(page.imageName)
.resizable()
.frame(width: page.imageWidth, height: page.imageWidth)
.cornerRadius(page.imageWidth/2)
.clipped()
Text(page.content).font(.body).lineLimit(nil)
}.padding(60)
}
}
struct MockData {
static let title = "Eating grapes 101"
static let contentStrings = [
"Step 1. Break off a branch holding a few grapes and lay it on your plate.",
"Step 2. Put a grape in your mouth whole.",
"Step 3. Deposit the seeds into your thumb and first two fingers.",
"Step 4. Place the seeds on your plate."
]
static let imageNames = [
"screen 1",
"screen 2",
"screen 3",
"screen 4"
]
}