SwiftUI View method called but output missing - swift

I have the View AlphabetLetterDetail:
import SwiftUI
struct AlphabetLetterDetail: View {
var alphabetLetter: AlphabetLetter
var letterAnimView : LetterAnimationView
var body: some View {
VStack {
Button(action:
animateLetter
) {
Image(uiImage: UIImage(named: "alpha_be_1")!)
.resizable()
.scaledToFit()
.frame(width: 60.0, height: 120.0)
}
letterAnimView
}.navigationBarTitle(Text(verbatim: alphabetLetter.name), displayMode: .inline)
}
func animateLetter(){
print("tapped")
letterAnimView.timerWrite()
}
}
containing the View letterAnimView of Type LetterAnimationView:
import SwiftUI
struct LetterAnimationView: View {
#State var Robot : String = ""
let LETTER =
["alpha_be_1_81",
"alpha_be_1_82",
"alpha_be_1_83",
"alpha_be_1_84",
"alpha_be_1_85",
"alpha_be_1_86",
"alpha_be_1_87",
"alpha_be_1_88",
"alpha_be_1_89",
"alpha_be_1_90",
"alpha_be_1"]
var body: some View {
VStack(alignment:.center){
Image(Robot)
.resizable()
.frame(width: 80, height: 160, alignment: .center)
.onAppear(perform: timerWrite)
}
}
func timerWrite(){
var index = 0
let _ = Timer.scheduledTimer(withTimeInterval: 0.08, repeats: true) {(Timer) in
Robot = LETTER[index]
print("one frame")
index += 1
if (index > LETTER.count - 1){
Timer.invalidate()
}
}
}
}
This gives me a fine animation, as coded in func timerWrite() and performed by .onAppear(perform: timerWrite).
After commenting //.onAppear(perform: timerWrite) I try animating by clicking
Button(action: animateLetter)
but nothing happens.
Maybe I got two different instances of letterAnimView, if so why?
Can anybody of you competent guys intentify my mistake?
Regards - Klaus

You don't want to store Views, they are structs so they are copied. Instead, create an ObservableObject to encapsulate this functionality.
I created RobotModel here with other minor changes:
class RobotModel: ObservableObject {
private static let atlas = [
"alpha_be_1_81",
"alpha_be_1_82",
"alpha_be_1_83",
"alpha_be_1_84",
"alpha_be_1_85",
"alpha_be_1_86",
"alpha_be_1_87",
"alpha_be_1_88",
"alpha_be_1_89",
"alpha_be_1_90",
"alpha_be_1"
]
#Published private(set) var imageName: String
init() {
imageName = Self.atlas.last!
}
func timerWrite() {
var index = 0
let _ = Timer.scheduledTimer(withTimeInterval: 0.08, repeats: true) { [weak self] timer in
guard let self = self else { return }
self.imageName = Self.atlas[index]
print("one frame")
index += 1
if index > Self.atlas.count - 1 {
timer.invalidate()
}
}
}
}
struct AlphabetLetterDetail: View {
#StateObject private var robot = RobotModel()
let alphabetLetter: AlphabetLetter
var body: some View {
VStack {
Button(action: animateLetter) {
Image(uiImage: UIImage(named: "alpha_be_1")!)
.resizable()
.scaledToFit()
.frame(width: 60.0, height: 120.0)
}
LetterAnimationView(robot: robot)
}.navigationBarTitle(Text(verbatim: alphabetLetter.name), displayMode: .inline)
}
func animateLetter() {
print("tapped")
robot.timerWrite()
}
}
struct LetterAnimationView: View {
#ObservedObject var robot: RobotModel
var body: some View {
VStack(alignment:.center){
Image(robot.imageName)
.resizable()
.frame(width: 80, height: 160, alignment: .center)
.onAppear(perform: robot.timerWrite)
}
}
}

Related

AnyTransition issue with simple view update in macOS

I have 2 user view called user1 and user2, I am updating user with button, and I want give a transition animation to update, but for some reason my transition does not work, as I wanted, the issue is there that Text animated correctly but image does not, it stay in its place and it does not move with Text to give a smooth transition animation.
struct ContentView: View {
#State var show: Bool = Bool()
var body: some View {
VStack {
if (show) {
UserView(label: { Text("User 1") })
.transition(AnyTransition.asymmetric(insertion: AnyTransition.move(edge: Edge.trailing), removal: AnyTransition.move(edge: Edge.leading)))
}
else {
UserView(label: { Text("User 2") })
.transition(AnyTransition.asymmetric(insertion: AnyTransition.move(edge: Edge.leading), removal: AnyTransition.move(edge: Edge.trailing)))
}
Button("update") { show.toggle() }
}
.padding()
.animation(Animation.linear(duration: 1.0), value: show)
}
}
struct UserView<Label: View>: View {
let label: () -> Label
#State private var heightOfLabel: CGFloat? = nil
var body: some View {
HStack {
if let unwrappedHeight: CGFloat = heightOfLabel {
Image(systemName: "person")
.resizable()
.frame(width: unwrappedHeight, height: unwrappedHeight)
}
label()
.background(GeometryReader { proxy in
Color.clear
.onAppear(perform: { heightOfLabel = proxy.size.height })
})
Spacer(minLength: CGFloat.zero)
}
.animation(nil, value: heightOfLabel)
}
}
the heightOfLabel doesn't have to be optional, and then it works:
struct UserView<Label: View>: View {
let label: () -> Label
#State private var heightOfLabel: CGFloat = .zero // not optional
var body: some View {
HStack {
Image(systemName: "person")
.resizable()
.frame(width: heightOfLabel, height: heightOfLabel)
label()
.background(GeometryReader { proxy in
Color.clear
.onAppear(perform: { heightOfLabel = proxy.size.height })
})
Spacer(minLength: CGFloat.zero)
}
.animation(nil, value: heightOfLabel)
}
}

How to have ForEach in view updated when array values change with SwiftUI

I have this code here for updating an array and a dictionary based on a timer:
class applicationManager: ObservableObject {
static var shared = applicationManager()
#Published var applicationsDict: [NSRunningApplication : Int] = [:]
#Published var keysArray: [NSRunningApplication] = []
}
class TimeOpenManager {
var secondsElapsed = 0.0
var timer = Timer()
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
self.secondsElapsed += 1
let applicationName = ws.frontmostApplication?.localizedName!
let applicationTest = ws.frontmostApplication
let appMan = applicationManager()
if let appData = applicationManager.shared.applicationsDict[applicationTest!] {
applicationManager.shared.applicationsDict[applicationTest!] = appData + 1
} else {
applicationManager.shared.applicationsDict[applicationTest!] = 1
}
applicationManager.shared.keysArray = Array(applicationManager.shared.applicationsDict.keys)
}
}
}
It works fine like this and the keysArray is updated with the applicationsDict keys when the timer is running. But even though keysArray is updated, the HStack ForEach in WeekView does not change when values are added.
I also have this view where keysArray is being loaded:
struct WeekView: View {
static let shared = WeekView()
var body: some View {
VStack {
HStack(spacing: 20) {
ForEach(0..<8) { day in
if day == weekday {
Text("\(currentDay)")
.frame(width: 50, height: 50, alignment: .center)
.font(.system(size: 36))
.background(Color.red)
.onTapGesture {
print(applicationManager.shared.keysArray)
print(applicationManager.shared.applicationsDict)
}
} else {
Text("\(currentDay + day - weekday)")
.frame(width: 50, height: 50, alignment: .center)
.font(.system(size: 36))
}
}
}
HStack {
ForEach(applicationManager.shared.keysArray, id: \.self) {dictValue in
Image(nsImage: dictValue.icon!)
.resizable()
.frame(width: 64, height: 64, alignment: .center)
}
}
}
}
}
As mentioned in the comments, applicationManager should be an ObservableObject with #Published properties.
Then, you need to tell your view that it should look for updates from this object. You can do that with the #ObservedObject property wrapper:
struct WeekView: View {
#ObservedObject private var manager = applicationManager.shared
And later:
ForEach(manager.keysArray) {
Replace anywhere you had applicatoinManager.shared with manager within the WeekView
Additional reading: https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-observedobject-to-manage-state-from-external-objects

Custom Segmented Controller SwiftUI Frame Issue

I would like to create a custom segmented controller in SwiftUI, and I found one made from this post. After slightly altering the code and putting it into my ContentView, the colored capsule would not fit correctly.
Here is an example of my desired result:
This is the result when I use it in ContentView:
CustomPicker.swift:
struct CustomPicker: View {
#State var selectedIndex = 0
var titles = ["Item #1", "Item #2", "Item #3", "Item #4"]
private var colors = [Color.red, Color.green, Color.blue, Color.purple]
#State private var frames = Array<CGRect>(repeating: .zero, count: 4)
var body: some View {
VStack {
ZStack {
HStack(spacing: 4) {
ForEach(self.titles.indices, id: \.self) { index in
Button(action: { self.selectedIndex = index }) {
Text(self.titles[index])
.foregroundColor(.black)
.font(.system(size: 16, weight: .medium, design: .default))
.bold()
}.padding(EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16)).background(
GeometryReader { geo in
Color.clear.onAppear { self.setFrame(index: index, frame: geo.frame(in: .global)) }
}
)
}
}
.background(
Capsule().fill(
self.colors[self.selectedIndex].opacity(0.4))
.frame(width: self.frames[self.selectedIndex].width,
height: self.frames[self.selectedIndex].height, alignment: .topLeading)
.offset(x: self.frames[self.selectedIndex].minX - self.frames[0].minX)
, alignment: .leading
)
}
.animation(.default)
.background(Capsule().stroke(Color.gray, lineWidth: 3))
}
}
func setFrame(index: Int, frame: CGRect) {
self.frames[index] = frame
}
}
ContentView.swift:
struct ContentView: View {
#State var itemsList = [Item]()
func loadData() {
if let url = Bundle.main.url(forResource: "Data", withExtension: "json") {
do {
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
let jsonData = try decoder.decode(Response.self, from: data)
for post in jsonData.content {
self.itemsList.append(post)
}
} catch {
print("error:\(error)")
}
}
}
var body: some View {
NavigationView {
VStack {
Text("Item picker")
.font(.system(.title))
.bold()
CustomPicker()
Spacer()
ScrollView {
VStack {
ForEach(itemsList) { item in
ItemView(text: item.text, username: item.username)
.padding(.leading)
}
}
}
.frame(height: UIScreen.screenHeight - 224)
}
.onAppear(perform: loadData)
}
}
}
Project file here
The problem with the code as-written is that the GeometryReader value is only sent on onAppear. That means that if any of the views around it change and the view is re-rendered (like when the data is loaded), those frames will be out-of-date.
I solved this by using a PreferenceKey instead, which will run on each render:
struct CustomPicker: View {
#State var selectedIndex = 0
var titles = ["Item #1", "Item #2", "Item #3", "Item #4"]
private var colors = [Color.red, Color.green, Color.blue, Color.purple]
#State private var frames = Array<CGRect>(repeating: .zero, count: 4)
var body: some View {
VStack {
ZStack {
HStack(spacing: 4) {
ForEach(self.titles.indices, id: \.self) { index in
Button(action: { self.selectedIndex = index }) {
Text(self.titles[index])
.foregroundColor(.black)
.font(.system(size: 16, weight: .medium, design: .default))
.bold()
}
.padding(EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16))
.measure() // <-- Here
.onPreferenceChange(FrameKey.self, perform: { value in
self.setFrame(index: index, frame: value) //<-- this will run each time the preference value changes, will will happen any time the frame is updated
})
}
}
.background(
Capsule().fill(
self.colors[self.selectedIndex].opacity(0.4))
.frame(width: self.frames[self.selectedIndex].width,
height: self.frames[self.selectedIndex].height, alignment: .topLeading)
.offset(x: self.frames[self.selectedIndex].minX - self.frames[0].minX)
, alignment: .leading
)
}
.animation(.default)
.background(Capsule().stroke(Color.gray, lineWidth: 3))
}
}
func setFrame(index: Int, frame: CGRect) {
print("Setting frame: \(index): \(frame)")
self.frames[index] = frame
}
}
struct FrameKey : PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
value = nextValue()
}
}
extension View {
func measure() -> some View {
self.background(GeometryReader { geometry in
Color.clear
.preference(key: FrameKey.self, value: geometry.frame(in: .global))
})
}
}
Note that the original .background call was taken out and was replaced with .measure() and .onPreferenceChange -- look for where the //<-- Here note is.
Besides that and the PreferenceKey and View extension, nothing else is changed.

Going to another View from SwiftUI to UIKit?

I use uihostingcontroller to load a SwiftUI view in a UIKit View.
in my SwiftUI view, I create some horizontal ScrollViews with some stuff in them.
I need to be able to click/tap on these elements and go to another view in my UIKit.
Is this possible?
I found this but this shows to "reload" the UIKit into the SwiftUI view which is not what I want to do and I don't think this is the correct way of doing this anyway:
Is there any way to change view from swiftUI to UIKit?
This is my SwiftUI code:
import SwiftUI
struct videosContentView: View {
var body: some View {
ScrollView{
ForEach(0..<2) {_ in
Section(header: Text("Important tasks")) {
VStack{
ScrollView(.horizontal){
HStack(spacing: 20) {
ForEach(0..<10) {
Text("Item \($0)")
.font(.headline)
.frame(width: 160, height: 200)
.background(Color.gray)
/*.padding()*/
.addBorder(Color.white, width: 1, cornerRadius: 10)
/*.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(Color.blue, lineWidth: 4)
)*/
}
}
}
}
}
}
}
}
}
struct videosContentView_Previews: PreviewProvider {
static var previews: some View {
videosContentView()
}
}
extension View {
public func addBorder<S>(_ content: S, width: CGFloat = 1, cornerRadius: CGFloat) -> some View where S : ShapeStyle {
let roundedRect = RoundedRectangle(cornerRadius: cornerRadius)
return clipShape(roundedRect)
.overlay(roundedRect.strokeBorder(content, lineWidth: width))
}
}
EDIT:
Based on suggestion in the comments, I tried this but this doesn't work:
Button(action: {
let secondViewController = self.storyboard.instantiateViewControllerWithIdentifier("home") as home
self.navigationController.pushViewController(secondViewController, animated: true)
}) {
Text("Dismiss me")
.font(.headline)
.frame(width: 160, height: 200)
.background(Color.gray)
.addBorder(Color.white, width: 1, cornerRadius: 10)
}
struct YourSwiftUIView: View {
#State var push = false
var body: some View {
if push {
YourUIViewController()
}else {
//your content
Button(action: {
withAnimation() {
push.toggle()
}
}) {
Text("Dismiss me")
.font(.headline)
.frame(width: 160, height: 200)
.background(Color.gray)
}
}
}
}
struct YourUIViewController: UIViewControllerRepresentable {
typealias UIViewControllerType = UIViewController
func makeUIViewController(context: UIViewControllerRepresentableContext<YourUIViewController>) -> UIViewController {
let yourUIViewController = UIViewController() //your UIViewController
return yourUIViewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<YourUIViewController>) {
}
}
this will change from the swiftuiview to the UIViewController.

#Environment(\.presentationMode) var mode: Binding<PresentationMode> Messing other views

I have a MailView()
import Foundation
import SwiftUI
import UIKit
import MessageUI
struct MailView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentation
#Binding var result: Result<MFMailComposeResult, Error>?
let newSubject : String
let newMsgBody : String
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
#Binding var presentation: PresentationMode
#Binding var result: Result<MFMailComposeResult, Error>?
init(presentation: Binding<PresentationMode>,
result: Binding<Result<MFMailComposeResult, Error>?>) {
_presentation = presentation
_result = result
}
func mailComposeController(_ controller: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult,
error: Error?) {
defer {
$presentation.wrappedValue.dismiss()
}
guard error == nil else {
self.result = .failure(error!)
return
}
self.result = .success(result)
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentation: presentation,
result: $result)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
let vc = MFMailComposeViewController()
vc.mailComposeDelegate = context.coordinator
vc.setToRecipients(["hello#email.co.uk"])
vc.setSubject(newSubject)
vc.setMessageBody(newMsgBody, isHTML: false)
return vc
}
func updateUIViewController(_ uiViewController: MFMailComposeViewController,
context: UIViewControllerRepresentableContext<MailView>) {
}
}
And In my SettingViews its called like so:
import SwiftUI
import URLImage
import UIKit
import MessageUI
struct SettingsView: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
#State private var showMailSheet = false
#State var result: Result<MFMailComposeResult, Error>? = nil
#State private var subject: String = ""
#State private var emailBody: String = ""
#EnvironmentObject var session: SessionStore
var body: some View {
NavigationView {
VStack(alignment: .leading) {
List {
Section(header: Text("Account")) {
NavigationLink(destination: ProfileView()) {
HStack {
Image(systemName: "person")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Edit Profile").font(.callout).fontWeight(.medium)
}
}.padding([.top,.bottom],5).padding(.trailing,10)
}
NavigationLink(destination: AccountView()) {
HStack {
Image(systemName: "doc")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("View Account").font(.callout).fontWeight(.medium)
}
}.padding([.top,.bottom],5).padding(.trailing,10)
}
NavigationLink(destination: PreferencesView()) {
HStack {
Image(systemName: "slider.horizontal.3")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Preferences").font(.callout).fontWeight(.medium)
}
}.padding([.top,.bottom],5).padding(.trailing,10)
}
}
Section(header: Text("Support")) {
HStack {
Image(systemName: "bubble.right")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Contact Us").font(.callout).fontWeight(.medium)
}
Spacer()
Button(action: {
self.subject = "Hello"
self.sendEmail()
}) {
Text("Send").font(.system(size:12))
}
}
HStack {
Image(systemName: "ant")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Report An Issue").font(.callout).fontWeight(.medium)
}
Spacer()
Button(action: {
self.sendEmail()
self.subject = "Report Issue"
self.emailBody = "Im having the following issues:"
}) {
Text("Report").font(.system(size:12))
}
}
}
Section (header: Text("Legal")) {
HStack {
Image(systemName: "hand.raised")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Privacy Policy").font(.callout).fontWeight(.medium)
}
Spacer()
Button(action: {
if let url = URL(string: "http://www.mysite.co.uk/privacy.html") {
UIApplication.shared.open(url)
}
}) {
Text("View").font(.system(size:12))
}
}
HStack {
Image(systemName: "folder")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Terms and Conditions (EULA)").font(.callout).fontWeight(.medium)
}
Spacer()
Button(action: {
if let url = URL(string: "http://www.mysite.co.uk/eula.html") {
UIApplication.shared.open(url)
}
}) {
Text("View").font(.system(size:12))
}
}
}
}.listStyle(GroupedListStyle())
}.navigationBarTitle("Settings", displayMode: .inline)
.background(NavigationBarConfigurator())
}.sheet(isPresented: $showMailSheet) {
MailView(result: self.$result, newSubject: self.subject, newMsgBody: self.emailBody)
}
}
func sendEmail() {
if MFMailComposeViewController.canSendMail() {
self.showMailSheet = true
} else {
print("Error sending mail")
}
}
}
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
SettingsView()
}
}
My Sheet is appearing nicely and once the email is sent, the sheet is dismissed as expected, but the following is causing an issue:
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
When I click:
NavigationLink(destination: ProfileView()) {
HStack {
Image(systemName: "person")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Edit Profile").font(.callout).fontWeight(.medium)
}
}.padding([.top,.bottom],5).padding(.trailing,10)
}
There is a action sheet:
.actionSheet(isPresented: self.$profileViewModel.showActionSheet){
ActionSheet(title: Text("Add a profile image"), message: nil, buttons: [
.default(Text("Camera"), action: {
self.profileViewModel.showImagePicker = true
self.sourceType = .camera
}),
.default(Text("Photo Library"), action: {
self.profileViewModel.showImagePicker = true
self.sourceType = .photoLibrary
}),
.cancel()
])
}.sheet(isPresented: self.$profileViewModel.showImagePicker){
imagePicker(image: self.$profileViewModel.upload_image, showImagePicker: self.$profileViewModel.showImagePicker, sourceType: self.sourceType)
}
When i click this button it keeps dismissing the button and I can't click on the options presented.
Any idea how I can have the #Environment(\.presentationMode) var mode: Binding<PresentationMode> only effecting the dismissing of the email? and not interfering with anything else?
#Environment(\.presentationMode) should be used for the last child view that you want to have this custom behaviour.
Any child view from where you declared the #Environment(\.presentationMode), will also inherit the same behaviour.
If you declare it only in MailView, it should fix it.