SwiftUI multiple vertical lists row animations not working - ios

Suddenly I have figured out that there is no automatic animation for non of them:
no animations for inserting, removing, reordering, etc. โ˜น๏ธ Ids are unique and persistent between transitions
Question 1
How can I make them work as expected?
Question 2
Is there a simple way to have an animation for seeing the transition between lists? Like moving a row between sections of a single list.
Explaining:
Let's say I have three lists that group different states of elements of a single array:
extension Call {
enum State: Equatable {
case inProgress
case accepted
case rejected
}
}
The observable:
class CallManager: ObservableObject {
#Published var calls: [Call]
init(visits: [Call] = []) { self.calls = visits }
}
And Call is a simple Identifiable:
struct Call: Identifiable & Equatable & Hashable {
let id = UUID()
var state: State
}
By making these bindings, I have bind all lists to the core calls array:
extension CallManager {
func bindingCalls(for state: Call.State) -> Binding<[Call]> {
Binding<[Call]>(
get: { self.calls.filter { $0.state == state } },
set: { // TODO: Find a better way for this
self.calls.removeAll(where: { $0.state == state })
self.calls.append(contentsOf: $0)
}
)
}
var inProgress: Binding<[Call]> { bindingCalls(for: .inProgress) }
var accepted: Binding<[Call]> { bindingCalls(for: .accepted) }
var rejected: Binding<[Call]> { bindingCalls(for: .rejected) }
}
And here is the View code:
struct ContentView: View {
#StateObject var visitManager = CallManager(visits: [
Call(state: .inProgress),
Call(state: .accepted),
Call(state: .accepted),
Call(state: .inProgress),
Call(state: .inProgress),
Call(state: .rejected)
])
var body: some View {
HStack {
List(visitManager.inProgress) { $call in
CallView(call: $call)
}
List(visitManager.accepted) { $call in
CallView(call: $call)
}
List(visitManager.rejected) { $call in
CallView(call: $call)
}
}
}
}
struct CallView: View & Identifiable {
#Binding var call: Call
var id: UUID { call.id }
var body: some View {
Text(call.id.uuidString.prefix(15))
.foregroundColor(call.state.color)
.onTapGesture(count: 2) { call.state = .rejected }
.onTapGesture { call.state = .accepted }
}
}
extension Call.State {
var color: Color {
switch self {
case .inProgress: return .blue
case .rejected: return .red
case .accepted: return .green
}
}
}

You can enable the animations on the List view:
List(visitManager.inProgress) { $call in
CallView(call: $call)
}
.animation(.default)
Or wrap the changes in a withAnimation block:
.onTapGesture { withAnimation { call.state = .accepted } }
As for the animation between the columns, you can get something like that with .matchedGeometryEffect. afaik it will always look a bit crumbly between List, to make it look good you need to use a VStack (but then loose all the comfort of the List view). For example:
import SwiftUI
extension Call {
enum State: Equatable, CaseIterable {
case inProgress
case accepted
case rejected
}
}
class CallManager: ObservableObject {
#Published var calls: [Call]
init(visits: [Call] = []) { calls = visits }
}
struct Call: Identifiable & Equatable & Hashable {
let id = UUID()
var state: State
}
struct ContentView: View {
#Namespace var items
#StateObject var visitManager = CallManager(visits: [
Call(state: .inProgress),
Call(state: .accepted),
Call(state: .accepted),
Call(state: .inProgress),
Call(state: .inProgress),
Call(state: .rejected),
])
var body: some View {
HStack(alignment: .top) {
ForEach(Call.State.allCases, id: \.self) { state in
VStack {
List(visitManager.calls.filter { $0.state == state }) { call in
CallView(call: call)
.id(call.id)
.matchedGeometryEffect(id: call.id, in: items)
.onTapGesture {
if let idx = visitManager.calls.firstIndex(where: { $0.id == call.id }) {
withAnimation {
visitManager.calls[idx].state = .rejected
}
}
}
}
}
}
}
}
}
struct CallView: View & Identifiable {
var call: Call
var id: UUID { call.id }
var body: some View {
Text(call.id.uuidString.prefix(15))
.foregroundColor(call.state.color)
}
}
extension Call.State {
var color: Color {
switch self {
case .inProgress: return .blue
case .rejected: return .red
case .accepted: return .green
}
}
}

Related

Using NavigationLink programmatically fails on iPadOS when using #StateObject in detail view

The following code works perfectly fine on iOS, but not on iPadOS. When I tap on one of the items in the list, the corresponding detail view is shown, but it will not change if I tap on another item. When I change the model in the LanguageDetail view to #ObservedObject, it works. To be clear, this is only an example to illustrate the problem. In my actual project, I'm not able to make this change though. The code below demonstrates this problem.
struct ContentView: View {
let languages: [String] = ["Objective-C", "Java", "Python", "Swift", "Rust"]
#State var selectedLanguage: String?
var body: some View {
NavigationView {
List(languages, id: \.self) { language in
Button(action: {selectedLanguage = language}) {
Text(language)
.bold()
.padding()
}
}
.background {
NavigationLink(isActive: $selectedLanguage.isPresent()) {
if let lang = selectedLanguage {
LanguageDetail(model: LanguageDetailModel(languageName: lang))
} else {
EmptyView()
}
} label: {
EmptyView()
}
}
}
}
}
struct LanguageDetail: View {
#StateObject var model: LanguageDetailModel
var body: some View {
VStack {
Text(model.languageName)
.font(.headline)
Text("to rule them all...")
}
}
}
class LanguageDetailModel: ObservableObject {
#Published var languageName: String
init(languageName: String) {
self.languageName = languageName
}
}
This extension is needed:
/// This extension is from the [SwiftUI Navigation Project on Github](https://github.com/pointfreeco/swiftui-navigation)
extension Binding {
/// Creates a binding by projecting the current optional value to a boolean describing if it's
/// non-`nil`.
///
/// Writing `false` to the binding will `nil` out the base value. Writing `true` does nothing.
///
/// - Returns: A binding to a boolean. Returns `true` if non-`nil`, otherwise `false`.
public func isPresent<Wrapped>() -> Binding<Bool>
where Value == Wrapped? {
.init(
get: { self.wrappedValue != nil },
set: { isPresent, transaction in
if !isPresent {
self.transaction(transaction).wrappedValue = nil
}
}
)
}
}
import SwiftUI
enum Language: Int, Identifiable, CaseIterable {
case objc
case java
case python
case swift
case rust
var id: Self { self }
var localizedDescription: LocalizedStringKey {
switch self {
case .objc: return "Objective-C"
case .java: return "Java"
case .python: return "Python"
case .swift: return "Swift"
case .rust: return "Rust"
}
}
}
struct LanguagesTest: View {
#State private var selection: Language?
var body: some View {
NavigationSplitView {
List(Language.allCases, selection: $selection) { language in
NavigationLink(value: language) {
Text(language.localizedDescription)
}
}
} detail: {
if let language = selection {
LanguageDetail(language: language)
}
else {
Text("Select Language")
}
}
}
}
struct LanguageDetail: View {
let language: Language
var body: some View {
VStack {
Text(language.localizedDescription)
.font(.headline)
Text("to rule them all...")
}
}
}

Encoding to JSON format is not encoding the toggled boolean value in Swift

I am making an app that has information about different woods, herbs and spices, and a few other things. I am including the ability to save their favorite item to a favorites list, so I have a heart button that the user can press to add it to the favorites. Pressing the button toggles the isFavorite property of the item and then leaving the page calls a method that encodes the data to save it to the user's device. The problem that I am running into is that it is not encoding the updated value of the isFavorite property. It is still encoding the value as false, so the favorites list is not persisting after closing and reopening the app.
Here is my Wood.swift code, this file sets up the structure for Wood items. I also included the test data that I was using to make sure that it displayed properly in the Wood extension:
import Foundation
struct Wood: Identifiable, Codable {
var id = UUID()
var mainInformation: WoodMainInformation
var preparation: [Preparation]
var isFavorite = false
init(mainInformation: WoodMainInformation, preparation: [Preparation]) {
self.mainInformation = mainInformation
self.preparation = preparation
}
}
struct WoodMainInformation: Codable {
var category: WoodCategory
var description: String
var medicinalUses: [String]
var magicalUses: [String]
var growZone: [String]
var lightLevel: String
var moistureLevel: String
var isPerennial: Bool
var isEdible: Bool
}
enum WoodCategory: String, CaseIterable, Codable {
case oak = "Oak"
case pine = "Pine"
case cedar = "Cedar"
case ash = "Ash"
case rowan = "Rowan"
case willow = "Willow"
case birch = "Birch"
}
enum Preparation: String, Codable {
case talisman = "Talisman"
case satchet = "Satchet"
case tincture = "Tincture"
case salve = "Salve"
case tea = "Tea"
case ointment = "Ointment"
case incense = "Incense"
}
extension Wood {
static let woodTypes: [Wood] = [
Wood(mainInformation: WoodMainInformation(category: .oak,
description: "A type of wood",
medicinalUses: ["Healthy", "Killer"],
magicalUses: ["Spells", "Other Witchy Stuff"],
growZone: ["6A", "5B"],
lightLevel: "Full Sun",
moistureLevel: "Once a day",
isPerennial: false,
isEdible: true),
preparation: [Preparation.incense, Preparation.satchet]),
Wood(mainInformation: WoodMainInformation(category: .pine,
description: "Another type of wood",
medicinalUses: ["Healthy"],
magicalUses: ["Spells"],
growZone: ["11G", "14F"],
lightLevel: "Full Moon",
moistureLevel: "Twice an hour",
isPerennial: true,
isEdible: true),
preparation: [Preparation.incense, Preparation.satchet])
]
}
Here is my WoodData.swift file, this file contains methods that allow the app to display the correct wood in the list of woods, as well as encode, and decode the woods:
import Foundation
class WoodData: ObservableObject {
#Published var woods = Wood.woodTypes
var favoriteWoods: [Wood] {
woods.filter { $0.isFavorite }
}
func woods(for category: WoodCategory) -> [Wood] {
var filteredWoods = [Wood]()
for wood in woods {
if wood.mainInformation.category == category {
filteredWoods.append(wood)
}
}
return filteredWoods
}
func woods(for category: [WoodCategory]) -> [Wood] {
var filteredWoods = [Wood]()
filteredWoods = woods
return filteredWoods
}
func index(of wood: Wood) -> Int? {
for i in woods.indices {
if woods[i].id == wood.id {
return i
}
}
return nil
}
private var dataFileURL: URL {
do {
let documentsDirectory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
return documentsDirectory.appendingPathComponent("evergreenData")
}
catch {
fatalError("An error occurred while getting the url: \(error)")
}
}
func saveWoods() {
if let encodedData = try? JSONEncoder().encode(woods) {
do {
try encodedData.write(to: dataFileURL)
let string = String(data: encodedData, encoding: .utf8)
print(string)
}
catch {
fatalError("An error occurred while saving woods: \(error)")
}
}
}
func loadWoods() {
guard let data = try? Data(contentsOf: dataFileURL) else { return }
do {
let savedWoods = try JSONDecoder().decode([Wood].self, from: data)
woods = savedWoods
}
catch {
fatalError("An error occurred while loading woods: \(error)")
}
}
}
Finally, this is my WoodsDetailView.swift file, this file displays the information for the wood that was selected, as well as calls the method that encodes the wood data:
import SwiftUI
struct WoodsDetailView: View {
#Binding var wood: Wood
#State private var woodsData = WoodData()
var body: some View {
VStack {
List {
Section(header: Text("Description")) {
Text(wood.mainInformation.description)
}
Section(header: Text("Preparation Techniques")) {
ForEach(wood.preparation, id: \.self) { technique in
Text(technique.rawValue)
}
}
Section(header: Text("Edible?")) {
if wood.mainInformation.isEdible {
Text("Edible")
}
else {
Text("Not Edible")
}
}
Section(header: Text("Medicinal Uses")) {
ForEach(wood.mainInformation.medicinalUses.indices, id: \.self) { index in
let medicinalUse = wood.mainInformation.medicinalUses[index]
Text(medicinalUse)
}
}
Section(header: Text("Magical Uses")) {
ForEach(wood.mainInformation.magicalUses.indices, id: \.self) { index in
let magicalUse = wood.mainInformation.magicalUses[index]
Text(magicalUse)
}
}
Section(header: Text("Grow Zone")) {
ForEach(wood.mainInformation.growZone.indices, id: \.self) { index in
let zone = wood.mainInformation.growZone[index]
Text(zone)
}
}
Section(header: Text("Grow It Yourself")) {
Text("Water: \(wood.mainInformation.moistureLevel)")
Text("Needs: \(wood.mainInformation.lightLevel)")
if wood.mainInformation.isPerennial {
Text("Perennial")
}
else {
Text("Annual")
}
}
}
}
.navigationTitle(wood.mainInformation.category.rawValue)
.onDisappear {
woodsData.saveWoods()
}
.toolbar {
ToolbarItem {
HStack {
Button(action: {
wood.isFavorite.toggle()
}) {
Image(systemName: wood.isFavorite ? "heart.fill" : "heart")
}
}
}
}
}
}
struct WoodsDetailView_Previews: PreviewProvider {
#State static var wood = Wood.woodTypes[0]
static var previews: some View {
WoodsDetailView(wood: $wood)
}
}
This is my MainTabView.swift file:
import SwiftUI
struct MainTabView: View {
#StateObject var woodData = WoodData()
var body: some View {
TabView {
NavigationView {
List {
WoodsListView(viewStyle: .allCategories(WoodCategory.allCases))
}
}
.tabItem { Label("Main", systemImage: "list.dash")}
NavigationView {
List {
WoodsListView(viewStyle: .favorites)
}
.navigationTitle("Favorites")
}.tabItem { Label("Favorites", systemImage: "heart.fill")}
}
.environmentObject(woodData)
.onAppear {
woodData.loadWoods()
}
.preferredColorScheme(.dark)
}
}
struct MainTabView_Previews: PreviewProvider {
static var previews: some View {
MainTabView()
}
}
This is my WoodListView.swift file:
import SwiftUI
struct WoodsListView: View {
#EnvironmentObject private var woodData: WoodData
let viewStyle: ViewStyle
var body: some View {
ForEach(woods) { wood in
NavigationLink(wood.mainInformation.category.rawValue, destination: WoodsDetailView(wood: binding(for: wood)))
}
}
}
extension WoodsListView {
enum ViewStyle {
case favorites
case singleCategory(WoodCategory)
case allCategories([WoodCategory])
}
private var woods: [Wood] {
switch viewStyle {
case let .singleCategory(category):
return woodData.woods(for: category)
case let .allCategories(category):
return woodData.woods(for: category)
case .favorites:
return woodData.favoriteWoods
}
}
func binding(for wood: Wood) -> Binding<Wood> {
guard let index = woodData.index(of: wood) else {
fatalError("Wood not found")
}
return $woodData.woods[index]
}
}
struct WoodsListView_Previews: PreviewProvider {
static var previews: some View {
WoodsListView(viewStyle: .singleCategory(.ash))
.environmentObject(WoodData())
}
}
Any assistance into why it is not encoding the toggled isFavorite property will be greatly appreciated.
Your problem is that structs are value types in Swift. Essentially this means that the instance of Wood that you have in WoodsDetailView is not the same instance that is in your array in your model (WoodData); It is a copy (Technically, the copy is made as soon as you modify the isFavourite property).
In SwiftUI it is important to maintain separation of responsibilities between the view and the model.
Changing the favourite status of a Wood is something the view should ask the model to do.
This is where you have a second issue; In your detail view you are creating a separate instance of your model; You need to refer to a single instance.
You have a good start; you have put your model instance in the environment where views can access it.
First, change the detail view to remove the binding, refer to the model from the environment and ask the model to do the work:
struct WoodsDetailView: View {
var wood: Wood
#EnvironmentObject private var woodsData: WoodData
var body: some View {
VStack {
List {
Section(header: Text("Description")) {
Text(wood.mainInformation.description)
}
Section(header: Text("Preparation Techniques")) {
ForEach(wood.preparation, id: \.self) { technique in
Text(technique.rawValue)
}
}
Section(header: Text("Edible?")) {
if wood.mainInformation.isEdible {
Text("Edible")
}
else {
Text("Not Edible")
}
}
Section(header: Text("Medicinal Uses")) {
ForEach(wood.mainInformation.medicinalUses, id: \.self) { medicinalUse in
Text(medicinalUse)
}
}
Section(header: Text("Magical Uses")) {
ForEach(wood.mainInformation.magicalUses, id: \.self) { magicalUse in
Text(magicalUse)
}
}
Section(header: Text("Grow Zone")) {
ForEach(wood.mainInformation.growZone, id: \.self) { zone in
Text(zone)
}
}
Section(header: Text("Grow It Yourself")) {
Text("Water: \(wood.mainInformation.moistureLevel)")
Text("Needs: \(wood.mainInformation.lightLevel)")
if wood.mainInformation.isPerennial {
Text("Perennial")
}
else {
Text("Annual")
}
}
}
}
.navigationTitle(wood.mainInformation.category.rawValue)
.onDisappear {
woodsData.saveWoods()
}
.toolbar {
ToolbarItem {
HStack {
Button(action: {
self.woodsData.toggleFavorite(for: wood)
}) {
Image(systemName: wood.isFavorite ? "heart.fill" : "heart")
}
}
}
}
}
}
struct WoodsDetailView_Previews: PreviewProvider {
static var wood = Wood.woodTypes[0]
static var previews: some View {
WoodsDetailView(wood: wood)
}
}
I also got rid of the unnecessary use of indices when listing the properties.
Now, add a toggleFavorite function to your WoodData object:
func toggleFavorite(for wood: Wood) {
guard let index = self.woods.firstIndex(where:{ $0.id == wood.id }) else {
return
}
self.woods[index].isFavorite.toggle()
}
You can also remove the index(of wood:Wood) function (which was really just duplicating Array's firstIndex(where:) function) and the binding(for wood:Wood) function.
Now, not only does your code do what you want, but you have hidden the mechanics of toggling a favorite from the view; It simply asks for the favorite status to be toggled and doesn't need to know what this actually involves.

How does SwiftUI know which view's needed to redraw or not?

I have a question about process that SwiftUI does.
This code below is from a Stanford cs193p lecture.
// View
struct ContentView: View {
#ObservedObject var viewModel: EmojiMemoryGame
var body: some View {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 65, maximum: 100))]) {
// Set break point 1
ForEach(viewModel.cards) { card in
CardView(card: card)
.aspectRatio(2/3, contentMode: .fit)
.onTapGesture {
viewModel.choose(card)
}
}
}
}
.foregroundColor(.red)
.padding(.horizontal)
}
}
struct CardView: View {
let card: MemoryGame<String>.Card
var body: some View {
// Set break point 2
ZStack {
let shape = RoundedRectangle(cornerRadius: 20)
if card.isFaceUp {
shape.fill().foregroundColor(.white)
shape.strokeBorder(lineWidth: 3)
Text(card.content).font(.largeTitle)
} else if card.isMatched {
shape.opacity(0)
} else {
shape.fill()
}
}
}
}
// ViewModel
class EmojiMemoryGame: ObservableObject {
#Published private var model: MemoryGame<String> = EmojiMemoryGame.createMemoryGame()
static let emojis = ["๐Ÿ‘", "๐Ÿ–ฅ", "โ™ฅ๏ธ", "โ™ฆ๏ธ", "โ™ฃ๏ธ", "โ™ ๏ธ", "๐ŸŽ", "โŒ", "๐Ÿง‘๐Ÿปโ€๐Ÿ’ป", "๐Ÿ’ผ",
"รท", "โœ•", "โˆš", "๐Ÿ˜ญ", "โžก๏ธ", "๐Ÿ™Œ", "๐Ÿ†š", "๐Ÿ…ถ", "๐Ÿ“", "๐Ÿคœ"]
static func createMemoryGame() -> MemoryGame<String> {
return MemoryGame<String>(numberOfPairsOfCards: EmojiMemoryGame.emojis.count) { pairIndex in
EmojiMemoryGame.emojis[pairIndex]
}
}
var cards: [MemoryGame<String>.Card] {
return model.cards
}
func choose(_ card: MemoryGame<String>.Card) {
model.choose(card)
}
}
// Model
struct MemoryGame<CardContent: Equatable> {
private(set) var cards: [Card]
private var indexOfTheOneAndOnlyFaceUpCard: Int?
init(numberOfPairsOfCards: Int, createCardContent: (Int) -> CardContent) {
cards = []
for pairIndex in 0..<numberOfPairsOfCards {
let content = createCardContent(pairIndex)
cards.append(Card(content: content))
cards.append(Card(content: content))
}
}
mutating func choose(_ card: Card) {
if let chosenIndex = cards.firstIndex(where: { $0.id == card.id }),
!cards[chosenIndex].isFaceUp,
!cards[chosenIndex].isMatched
{
if let potentialMatchIndex = indexOfTheOneAndOnlyFaceUpCard {
if cards[chosenIndex].content == cards[potentialMatchIndex].content {
cards[chosenIndex].isMatched = true
cards[potentialMatchIndex].isMatched = true
}
indexOfTheOneAndOnlyFaceUpCard = nil
} else {
for index in cards.indices {
cards[index].isFaceUp = false
}
indexOfTheOneAndOnlyFaceUpCard = chosenIndex
}
cards[chosenIndex].isFaceUp.toggle()
}
}
struct Card: Identifiable {
let id: UUID = UUID()
var isFaceUp: Bool = false
var isMatched: Bool = false
var content: CardContent
}
}
As far as I know, viewModel.choose(card) is called, then some Card in cards(in MemoryGame) is changed. And next model(in EmojiMemoryGame) notices that I was changed, so finally ContentView will redraw the body(specifically some part that is depending on viewModel).
So I thought all CardView is going to be redrawn, but it's not. They are redrawing CardView only that has changed.
To double check, I set the break point like the comment in code above. It turns out, first break point came up for every card, but second break point just came up for just the card that had changed before.
Why does this happen like that?
Is it just SwiftUI's power?

Why is my .onAppear not getting triggered when an EnvironmentObject changes?

I'm trying to learn SwiftUI, but i can't seem to get my view to update. I want my WorkoutsView to refresh with the newly added workout when the user presses the "Add" button:
WorkoutTrackerApp:
#main
struct WorkoutTrackerApp: App {
var body: some Scene {
WindowGroup {
WorkoutTrackerView()
}
}
}
extension WorkoutTrackerApp {
struct WorkoutTrackerView: View {
#StateObject var workoutService = WorkoutService.instance
var body: some View {
NavigationView {
WorkoutsView { $workout in
NavigationLink(destination: WorkoutView(workout: $workout)){
Text(workout.title)
}
}
.toolbar {
Button("Add") {
workoutService.addNewWorkout()
}
}
.navigationTitle("Workouts")
}
.environmentObject(workoutService)
}
}
}
WorkoutsView:
import Foundation
import SwiftUI
struct WorkoutsView<Wrapper>: View where Wrapper: View {
#EnvironmentObject var workoutService: WorkoutService
#StateObject var viewModel: ViewModel
let workoutWrapper: (Binding<Workout>) -> Wrapper
init(_ viewModel: ViewModel = .init(), workoutWrapper: #escaping (Binding<Workout>) -> Wrapper) {
_viewModel = StateObject(wrappedValue: viewModel)
self.workoutWrapper = workoutWrapper
}
var body: some View {
List {
Section(header: Text("All Workouts")) {
ForEach($viewModel.workouts) { $workout in
workoutWrapper($workout)
}
}
}
.onAppear {
viewModel.workoutService = self.workoutService
viewModel.getWorkouts()
}
}
}
extension WorkoutsView {
class ViewModel: ObservableObject {
#Published var workouts = [Workout]()
var workoutService: WorkoutService?
func getWorkouts() {
workoutService?.getWorkouts { workouts in
self.workouts = workouts
}
}
}
}
WorkoutService:
import Foundation
class WorkoutService: ObservableObject {
static let instance = WorkoutService()
#Published var workouts = [Workout]()
private init() {
for i in 0...5 {
let workout = Workout(id: i, title: "Workout \(i)", exercises: [])
workouts.append(workout)
}
}
func getWorkouts(completion: #escaping ([Workout]) -> Void) {
DispatchQueue.main.async {
completion(self.workouts)
}
}
func addNewWorkout() {
let newWorkout = Workout(title: "New Workout")
workouts = workouts + [newWorkout]
}
}
The .onAppear in WorkoutsView only gets called once - when the view gets initialised for the first time. I want it to also get triggered when workoutService.addNewWorkout() gets called.
FYI: The WorkoutService is a 'mock' service, in the future i want to call an API there.
Figured it out, changed the body of WorkoutsView to this:
var body: some View {
List {
Section(header: Text("All Workouts")) {
ForEach($viewModel.workouts) { $workout in
workoutWrapper($workout)
}
}
}
.onAppear {
viewModel.workoutService = self.workoutService
viewModel.getWorkouts()
}
.onReceive(workoutService.objectWillChange) {
viewModel.getWorkouts()
}
}
Now the workouts list gets refreshed when workoutService publisher emits. The solution involved using the .onReceive to do something when the WorkoutService changes.

How to pass method handler back SwiftUI

I'm new at Swift and currently, I'm implementing UI for verification code but I have no idea to do I have found something that similar to my requirement on StackOverflow I just copy and pass into my project, and then as you can see in VerificationView_Previews we need to pass the method handler back i don't know how to pass it help, please
//
// VerificationView.swift
// UpdateHistory
//
// Created by Admin on 4/21/21.
//
import SwiftUI
public struct VerificationView: View {
var maxDigits: Int = 6
var label = "Enter One Time Password"
#State var pin: String = ""
#State var showPin = true
var handler: (String, (Bool) -> Void) -> Void
public var body: some View {
VStack {
Text(label).font(.title)
ZStack {
pinDots
backgroundField
}
}
}
private var pinDots: some View {
HStack {
Spacer()
ForEach(0..<maxDigits) { index in
Image(systemName: self.getImageName(at: index))
.font(.system(size: 60, weight: .thin, design: .default))
Spacer()
}
}
}
private func getImageName(at index: Int) -> String {
if index >= self.pin.count {
return "square"
}
if self.showPin {
return self.pin.digits[index].numberString + ".square"
}
return "square"
}
private var backgroundField: some View {
let boundPin = Binding<String>(get: { self.pin }, set: { newValue in
self.pin = newValue
self.submitPin()
})
return TextField("", text: boundPin, onCommit: submitPin)
.accentColor(.clear)
.foregroundColor(.clear)
.keyboardType(.numberPad)
}
private var showPinButton: some View {
Button(action: {
self.showPin.toggle()
}, label: {
self.showPin ?
Image(systemName: "eye.slash.fill").foregroundColor(.primary) :
Image(systemName: "eye.fill").foregroundColor(.primary)
})
}
private func submitPin() {
if pin.count == maxDigits {
handler(pin) { isSuccess in
if isSuccess {
print("pin matched, go to next page, no action to perfrom here")
} else {
pin = ""
print("this has to called after showing toast why is the failure")
}
}
}
}
}
extension String {
var digits: [Int] {
var result = [Int]()
for char in self {
if let number = Int(String(char)) {
result.append(number)
}
}
return result
}
}
extension Int {
var numberString: String {
guard self < 10 else { return "0" }
return String(self)
}
}
struct VerificationView_Previews: PreviewProvider {
static var previews: some View {
VerificationView() // need to pass method handler
}
}
For viewing purpose you can just simply use like this. No need second param for preview
struct VerificationView_Previews: PreviewProvider {
static var previews: some View {
VerificationView { (pin, _) in
print(pin)
}
}
}
You can also use like this
struct VerificationView_Previews: PreviewProvider {
var successClosure: (Bool) -> Void
static var previews: some View {
VerificationView { (pin, successClosure) in
}
}
}

Resources