Cannot use mutating member on immutable value: 'self' is immutable in SwiftUI - swiftui

For the following code, I am getting the following error. I don't know how to work around this. How can I call volumeCheck() upon the button click?
struct ContentView: View {
var player = AVAudioPlayer()
var body: some View {
HStack {
Button(action: {
self.volumeCheck()
}) {
Text("Click to test chimes volume")
}
}
}
mutating func volumeCheck() {
guard let url = Bundle.main.url(
forResource: "chimes",
withExtension: "mp3"
) else { return }
do {
player = try AVAudioPlayer(contentsOf: url)
player.prepareToPlay()
player.volume = Float(self.sliderValue)
player.play()
} catch let error {
print(error.localizedDescription)
}
print("volume checked print")
}
}

The problem is that View is a struct and it's body field is a computed property with a nonmutating getter. In your code it happens mutating method to be called in that nonmutating getter. So all you need to do is put your player to some kind of model:
class Model {
var player: AVPlayerPlayer()
}
struct ContentView: View {
var model = Model()
// player can be changed from anywhere
}
P.S. In some other cases you may want changes in your model be reflected in view so you'd have to add #ObservedObject just before model declaration.
Hope that helps

You are trying to set player to a new object in volumeCheck(). Set the player in your initialiser:
struct ContentView: View {
private var player: AVAudioPlayer?
public init() {
if let url = Bundle.main.url(forResource: "chimes",
withExtension: "mp3") {
self.player = try? AVAudioPlayer(contentsOf: url)
}
}
var body: some View {
HStack {
Button("Click to test chimes volume") {
self.volumeCheck()
} .disabled(player == nil)
}
}
private func volumeCheck() {
player?.prepareToPlay()
player?.volume = Float(self.sliderValue)
player?.play()
print("volume checked print")
}
}
Please note that you are using sliderValue in your code even though it is not defined anywhere.

Related

Pass in default text in TextView while keeping state changes with SwiftUI

I am trying to set a default text on a TextView when the view appears, while being able to still keep track of changes to the TextView that I can then pass on to my ViewModel.
Here is a little example that looks like what I am trying to do. This does however not work, it does not update the state as I would have expected. Am I doing something wrong?
struct NoteView: View {
#State var note = ""
var noteFromOutside: String?
var body: some View {
VStack {
TextField("Write a note...", text: $note)
.onSubmit {
//Do something with the note.
}
}
.onAppear {
if let newNote = noteFromOutside {
note = newNote
}
}
}
}
struct ParentView: View {
var note = "Note"
var body: some View {
VStack {
NoteView(noteFromOutside: note)
}
}
}
Found this answer to another post which solved my problem. The key was in the #Binding and init().
https://stackoverflow.com/a/64526620/12764203

How to use data from 1 tab view to another tab view in Swiftui? Looking for a approach

I have 2 tabs and the associated views are tabAView and tabBView.
On tabAView, 1 API call is there and got user object which is Published object in its ViewModel. ViewModel name is UserViewModel. UserViewModel is being observed by tabAView.
On tabBView, I have to use that user object. Because on some actions, user object value is changed, those changes should be reflected on subsequent views.
I am confused about the environment object usage here. Please suggest what will be the best approach.
Here is the code to understand better my problem.
struct ContentView: View {
enum AppPage: Int {
case TabA=0, TabB=1
}
#StateObject var settings = Settings()
var viewModel: UserViewModel
var body: some View {
NavigationView {
TabView(selection: $settings.tabItem) {
TabAView(viewModel: viewModel)
.tabItem {
Text("TabA")
}
.tag(AppPage.TabA)
AppsView()
.tabItem {
Text("Apps")
}
.tag(AppPage.TabB)
}
.accentColor(.white)
.edgesIgnoringSafeArea(.top)
.onAppear(perform: {
settings.tabItem = .TabA
})
.navigationBarTitleDisplayMode(.inline)
}
.environmentObject(settings)
}
}
This is TabAView:
struct TabAView: View {
#ObservedObject var viewModel: UserViewModel
#EnvironmentObject var settings: Settings
init(viewModel: UserViewModel) {
self.viewModel = viewModel
}
var body: some View {
Vstack {
/// code
}
.onAppear(perform: {
/// code
})
.environmentObject(settings)
}
}
This is the UserViewModel where API is hit and user object comes:
class UserViewModel: ObservableObject {
private var apiService = APIService.shared
#Published var user: EndUserData?
init () {
getUserProfile()
}
func getUserProfile() {
apiService.getUserAccount() { user in
DispatchQueue.main.async {
self.user = user
}
}
}
}
Below is the APIService function, where the user object is saved into UserDefaults for use. Which I know is incorrect.(That is why I am looking for another solution). Hiding the URL, because of its confidential.
func getUserAccount(completion: #escaping (EndUserData?) -> Void) {
self.apiManager.makeRequest(toURL: url, withHttpMethod: .get) { results in
guard let response = results.response else { return completion(nil) }
if response.httpStatusCode == 200 {
guard let data = results.data else { return completion(nil) }
do {
let str = String(decoding: data, as: UTF8.self)
print(str)
let decoder = JSONDecoder()
let responseData = try decoder.decode(ResponseData<EndUserData>.self, from: data)
UserDefaults.standard.set(data, forKey: "Account")
completion(responseData.data)
} catch let jsonError as NSError {
print(jsonError.localizedDescription)
return completion(nil)
}
}
}
}
This is another TabBView:
struct TabBView: View {
var user: EndUserData?
init() {
do {
guard let data = UserDefaults.standard.data(forKey: "Account") else {
return
}
let decoder = JSONDecoder()
let responseData = try decoder.decode(ResponseData<EndUserData>.self, from: data)
user = responseData.data
} catch let jsonError as NSError {
print(jsonError.localizedDescription)
}
}
var body: some View {
VStack (spacing: 10) {
UserSearch()
}
}
}
This is another view in TabBView, where the User object is used. Changes are not reflecting here.
struct UserSearch: View {
private var user: EndUserData?
init(comingFromAppsSection: Bool) {
do {
guard let data = UserDefaults.standard.data(forKey: "Account") else {
return
}
let decoder = JSONDecoder()
let responseData = try decoder.decode(ResponseData<EndUserData>.self, from: data)
user = responseData.data
} catch let jsonError as NSError {
print(jsonError.localizedDescription)
}
}
var body: some View {
Vstack {
Text(user.status)
}
}
}
I have removed most of the code from a confidential point of view but this code will explain the reason and error. Please look into the code and help me.

SwiftUI: #Environment not receiving provided value down the view hierarchy

I am following the example of this project to create my iOS app (thanks Alexey!), but can't get the #Environment variable to receive the value that is being passed down the UI hierarchy. The top level view receives the correct value, but the downstream view receives the default value.
EDIT: After tying to replicate Asperi's code, I found that this behavior happens only when the downstream view is invoked via a NavigationLink. Updated the code below:
EDIT2: The problem was with where the environment method was being invoked. Invoking it on the NavigationView instead of the MainView solved the problem. Code updated below:
Custom Environment key - DIContainer
struct DIContainer: EnvironmentKey {
let interactor: Interactor
init(interactor: Interactor) {
self.interactor = interactor
}
static var defaultValue: Self { Self.default }
private static let `default` = Self(interactor: .stub)
}
extension EnvironmentValues {
var injected: DIContainer {
get { self[DIContainer.self] }
set { self[DIContainer.self] = newValue }
}
}
App struct
private let container: DIContainer
init() {
container = DIContainer(interactor: RealInteractor())
}
var body: some Scene {
WindowGroup {
NavigationView {
MainView()
}
.environment(\.injected, container)
}
Main View
struct MainView: View {
#Environment(\.injected) private var injected: DIContainer
// `injected` has the `RealInteractor`, as expected
var body: some View {
VStack {
Text("Main: \(injected.foo())") \\ << Prints REAL
NavigationLink(destination: SearchView()) {
Text("Search")
}
}
}
}
Search View
struct SearchView: View {
#Environment(\.injected) private var injected: DIContainer
// `injected` has the `StubInteractor`, why?
var body: some View {
Text("Search: \(injected.foo())")
}
}
I am able to solve this problem by modifying the MainView like so:
var body: some View {
SearchView()
.environment(\.injected, container)
}
But isn't avoiding doing this repeatedly the purpose of #Environment?
Any guidance/pointers appreciated.
I've tryied to replicate all parts and to make them compiled... and the result just works as expected - environment is passed down the view hierarchy, so you might miss something in your real code.
Here is complete module, tested with Xcode 12.4 / iOS 14.4
class Interactor { // << replicated !!
static let stub = Interactor()
func foo() -> String { "stub" }
}
class RealInteractor: Interactor { // << replicated !!
override func foo() -> String { "real" }
}
struct ContentView: View { // << replicated !!
private let container: DIContainer
init() {
container = DIContainer(interactor: RealInteractor())
}
var body: some View {
NavigationView {
MainView()
}
.environment(\.injected, container) // << to affect any links !!
}
}
// no changes in env parts
struct DIContainer: EnvironmentKey {
let interactor: Interactor
init(interactor: Interactor) {
self.interactor = interactor
}
static var defaultValue: Self { Self.default }
private static let `default` = Self(interactor: .stub)
}
extension EnvironmentValues {
var injected: DIContainer {
get { self[DIContainer.self] }
set { self[DIContainer.self] = newValue }
}
}
struct MainView: View {
#Environment(\.injected) private var injected: DIContainer
// `injected` has the `RealInteractor`, as expected
var body: some View {
SearchView()
}
}
// just tested here
struct SearchView: View {
#Environment(\.injected) private var injected: DIContainer
var body: some View {
Text("Result: \(injected.interactor.foo())") // << works fine !!
}
}

SwifUI onAppear gets called twice

Q1: Why are onAppears called twice?
Q2: Alternatively, where can I make my network call?
I have placed onAppears at a few different place in my code and they are all called twice. Ultimately, I'm trying to make a network call before displaying the next view so if you know of a way to do that without using onAppear, I'm all ears.
I have also tried to place and remove a ForEach inside my Lists and it doesn't change anything.
Xcode 12 Beta 3 -> Target iOs 14
CoreData enabled but not used yet
struct ChannelListView: View {
#EnvironmentObject var channelStore: ChannelStore
#State private var searchText = ""
#ObservedObject private var networking = Networking()
var body: some View {
NavigationView {
VStack {
SearchBar(text: $searchText)
.padding(.top, 20)
List() {
ForEach(channelStore.allChannels) { channel in
NavigationLink(destination: VideoListView(channel: channel)
.onAppear(perform: {
print("PREVIOUS VIEW ON APPEAR")
})) {
ChannelRowView(channel: channel)
}
}
.listStyle(GroupedListStyle())
}
.navigationTitle("Channels")
}
}
}
}
struct VideoListView: View {
#EnvironmentObject var videoStore: VideoStore
#EnvironmentObject var channelStore: ChannelStore
#ObservedObject private var networking = Networking()
var channel: Channel
var body: some View {
List(videoStore.allVideos) { video in
VideoRowView(video: video)
}
.onAppear(perform: {
print("LIST ON APPEAR")
})
.navigationTitle("Videos")
.navigationBarItems(trailing: Button(action: {
networking.getTopVideos(channelID: channel.channelId) { (videos) in
var videoIdArray = [String]()
videoStore.allVideos = videos
for video in videoStore.allVideos {
videoIdArray.append(video.videoID)
}
for (index, var video) in videoStore.allVideos.enumerated() {
networking.getViewCount(videoID: videoIdArray[index]) { (viewCount) in
video.viewCount = viewCount
videoStore.allVideos[index] = video
networking.setVideoThumbnail(video: video) { (image) in
video.thumbnailImage = image
videoStore.allVideos[index] = video
}
}
}
}
}) {
Text("Button")
})
.onAppear(perform: {
print("BOTTOM ON APPEAR")
})
}
}
I had the same exact issue.
What I did was the following:
struct ContentView: View {
#State var didAppear = false
#State var appearCount = 0
var body: some View {
Text("Appeared Count: \(appearrCount)"
.onAppear(perform: onLoad)
}
func onLoad() {
if !didAppear {
appearCount += 1
//This is where I loaded my coreData information into normal arrays
}
didAppear = true
}
}
This solves it by making sure only what's inside the the if conditional inside of onLoad() will run once.
Update: Someone on the Apple Developer forums has filed a ticket and Apple is aware of the issue. My solution is a temporary hack until Apple addresses the problem.
I've been using something like this
import SwiftUI
struct OnFirstAppearModifier: ViewModifier {
let perform:() -> Void
#State private var firstTime: Bool = true
func body(content: Content) -> some View {
content
.onAppear{
if firstTime{
firstTime = false
self.perform()
}
}
}
}
extension View {
func onFirstAppear( perform: #escaping () -> Void ) -> some View {
return self.modifier(OnFirstAppearModifier(perform: perform))
}
}
and I use it instead of .onAppear()
.onFirstAppear{
self.vm.fetchData()
}
you can create a bool variable to check if first appear
struct VideoListView: View {
#State var firstAppear: Bool = true
var body: some View {
List {
Text("")
}
.onAppear(perform: {
if !self.firstAppear { return }
print("BOTTOM ON APPEAR")
self.firstAppear = false
})
}
}
Let us assume you are now designing a SwiftUI and your PM is also a physicist and philosopher. One day he tells you we should to unify UIView and UIViewController, like Quantum Mechanics and the Theory of Relativity. OK, you are like-minded with your leader, voting for "Simplicity is Tao", and create an atom named "View". Now you say: "View is everything, view is all". That sounds awesome and seems feasible. Well, you commit the code and tell the PM….
onAppear and onDisAppear exists in every view, but what you really need is a Page lifecycle callback. If you use onAppear like viewDidAppear, then you get two problems:
Being influenced by the parent, the child view will rebuild more than one time, causing onAppear to be called many times.
SwiftUI is closed source, but you should know this: view = f(view). So, onAppear will run to return a new View, which is why onAppear is called twice.
I want to tell you onAppear is right! You MUST CHANGE YOUR IDEAS. Don’t run lifecycle code in onAppear and onDisAppear! You should run that code in the "Behavior area". For example, in a button navigating to a new page.
You can create the first appear function for this bug
extension View {
/// Fix the SwiftUI bug for onAppear twice in subviews
/// - Parameters:
/// - perform: perform the action when appear
func onFirstAppear(perform: #escaping () -> Void) -> some View {
let kAppearAction = "appear_action"
let queue = OperationQueue.main
let delayOperation = BlockOperation {
Thread.sleep(forTimeInterval: 0.001)
}
let appearOperation = BlockOperation {
perform()
}
appearOperation.name = kAppearAction
appearOperation.addDependency(delayOperation)
return onAppear {
if !delayOperation.isFinished, !delayOperation.isExecuting {
queue.addOperation(delayOperation)
}
if !appearOperation.isFinished, !appearOperation.isExecuting {
queue.addOperation(appearOperation)
}
}
.onDisappear {
queue.operations
.first { $0.name == kAppearAction }?
.cancel()
}
}
}
For everyone still having this issue and using a NavigationView. Add this line to the root NavigationView() and it should fix the problem.
.navigationViewStyle(StackNavigationViewStyle())
From everything I have tried, this is the only thing that worked.
We don't have to do it on .onAppear(perform)
This can be done on init of View
In case someone else is in my boat, here is how I solved it for now:
struct ChannelListView: View {
#State private var searchText = ""
#State private var isNavLinkActive: Bool = false
#EnvironmentObject var channelStore: ChannelStore
#ObservedObject private var networking = Networking()
var body: some View {
NavigationView {
VStack {
SearchBar(text: $searchText)
.padding(.top, 20)
List(channelStore.allChannels) { channel in
ZStack {
NavigationLink(destination: VideoListView(channel: channel)) {
ChannelRowView(channel: channel)
}
HStack {
Spacer()
Button {
isNavLinkActive = true
// Place action/network call here
} label: {
Image(systemName: "arrow.right")
}
.foregroundColor(.gray)
}
}
.listStyle(GroupedListStyle())
}
.navigationTitle("Channels")
}
}
}
}
I've got this app:
#main
struct StoriesApp: App {
var body: some Scene {
WindowGroup {
TabView {
NavigationView {
StoriesView()
}
}
}
}
}
And here is my StoriesView:
// ISSUE
struct StoriesView: View {
#State var items: [Int] = []
var body: some View {
List {
ForEach(items, id: \.self) { id in
StoryCellView(id: id)
}
}
.onAppear(perform: onAppear)
}
private func onAppear() {
///////////////////////////////////
// Gets called 2 times on app start <--------
///////////////////////////////////
}
}
I've resolved the issue by measuring the diff time between onAppear() calls. According to my observations double calls of onAppear() happen between 0.02 and 0.45 seconds:
// SOLUTION
struct StoriesView: View {
#State var items: [Int] = []
#State private var didAppearTimeInterval: TimeInterval = 0
var body: some View {
List {
ForEach(items, id: \.self) { id in
StoryCellView(id: id)
}
}
.onAppear(perform: onAppear)
}
private func onAppear() {
if Date().timeIntervalSince1970 - didAppearTimeInterval > 0.5 {
///////////////////////////////////////
// Gets called only once in 0.5 seconds <-----------
///////////////////////////////////////
}
didAppearTimeInterval = Date().timeIntervalSince1970
}
}
In my case, I found that a few views up the hierarchy, .onAppear() (and .onDisappear()) was only being called once, as expected. I used that to post notifications that I listen to down in the views that need to take action on those events. It’s a gross hack, and I’ve verified that the bug is fixed in iOS 15b1, but Apple really needs to backport the fix.

How can I make a ViewModel instance alive in SwiftUI?

In my app, there is a singleton instance, AppSetting, which is used in the entire views and models. AppSetting has a variable, userName.
class AppSetting: ObservableObject {
static let shared = AppSetting()
private init() { }
#Published var userName: String = ""
}
ParentView prints userName when it is not empty. At first, it is empty.
struct ParentView: View {
#State var isChildViewPresented = false
#ObservedObject var appSetting = AppSetting.shared
var body: some View {
ZStack {
Color.white.edgesIgnoringSafeArea(.all)
VStack {
Button(action: { self.isChildViewPresented = true }) {
Text("Show ChildView")
}
if !appSetting.userName.isEmpty { // <--- HERE!!!
Text("\(appSetting.userName)")
}
}
if isChildViewPresented {
ChildView(isPresented: $isChildViewPresented)
}
}
}
}
When a user taps the button, userName will be set.
struct ChildView: View {
#Binding var isPresented: Bool
#ObservedObject var childModel = ChildModel()
var body: some View {
ZStack {
Color.white.edgesIgnoringSafeArea(.all)
VStack {
Button(action: { self.childModel.setUserName() }) { // <--- TAP BUTTON HERE!!!
Text("setUserName")
}
Button(action: { self.isPresented = false }) {
Text("Close")
}
}
}
}
}
class ChildModel: ObservableObject {
init() { print("init") }
deinit { print("deinit") }
func setUserName() {
AppSetting.shared.userName = "StackOverflow" // <--- SET userName HERE!!!
}
}
The problem is when userName is set, the instance of ChildModel is invalidated. I think when ParentView adds Text("\(appSetting.userName)"), it changes its view hierarchy and then it makes SwiftUI delete the old instance of ChildModel and create a new one. Sadly, it gives me tons of bug. In my app, the ChildModel instance must be alive until a user explicitly closes ChildView.
How can I make the ChildModel instance alive?
Thanks in advance.
It is possible when to de-couple view & view model and inject dependency via constructor
struct ChildView: View {
#Binding var isPresented: Bool
#ObservedObject var childModel: ChildModel // don't initialize
// ... other your code here
store model somewhere externally and inject when show child view
if isChildViewPresented {
// inject ref to externally stored ChildModel()
ChildView(isPresented: $isChildViewPresented, viewModel: childModel)
}