Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 4 months ago.
Improve this question
I am confused about the timer in Swift. Can I set the 'selector' to a function in ContentView: View?
I has searched 1 day and all the method to achieve a timer using #objc before function. But when I add #objc before func update(), then Xcode will report an error for:
#objc can only be used with members of classes, #objc protocols, and concrete extensions of classes
I have searched 1 day and all the method to achieve a timer using #objc before function.
for future reference add suffix or tag "swiftui" before any search related to swiftui, so the search engine you are using gives you swiftui related results. It may still have solutions from UIKit or ObjC, but atleast few will give out right result
timer was initially written in Objc way before SwiftUI came into picture, which is why you are getting those results.
Combine came up with an entirely new Timer publisher method, which can be used here with swiftui. Added an example below
Try this instead
struct ContentView: View {
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State private var counter = 0
#State private var running = true
func begin() {
}
func pause() {
}
var body: some View {
VStack {
Text("You have used this app")
.font(.largeTitle)
.padding(.bottom)
Text(String(counter) + " s")
.font(.largeTitle)
.foregroundColor(.blue)
.padding(.top)
.onReceive(timer) { _ in
counter += 1
}
}
.padding()
}
}
Related
I am currently making an app that takes in three user inputs for a red, green, and blue color value between 0 and 255 through separate TextField views. Right now, the user can input the values fine but there is no way of tabbing out of the keyboards. I understand there are two main ways to go about this. The first is adding a return button to the keyboard and the second is exiting out through a tap gesture.
I am fairly new to SwiftUI and I keep seeing online that the best solution is to override the viewDidLoad function and set a tap gesture in there. I am honestly not sure what the viewDidLoad function is and I am still very confused after researching it. At the moment, I am also not very familiar with UIViewControllers.
Is there an easier way to solve my issue or will I have to use a UIViewController and override the viewDidLoad function?
enter code here
VStack {
TextField("255", text: $redC) { editing in
isEditing = editing
redV = (redC as NSString).doubleValue
} onCommit: {
redV = (redC as NSString).doubleValue
}
TextField("255", text: $greenC) { editing in
isEditing = editing
greenV = (greenC as NSString).doubleValue
} onCommit: {
greenV = (greenC as NSString).doubleValue
}
TextField("255", text: $blueC) { editing in
isEditing = editing
blueV = (blueC as NSString).doubleValue
} onCommit: {
blueV = (blueC as NSString).doubleValue
}
}
.multilineTextAlignment(.center)
.keyboardType(.numberPad)
Use onTapGesture closure
First Create an extension in UIApplication like that,
extension UIApplication {
func resignFirstResponder() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
Add this in VStack like that,
VStack {
// contents
}
.onTapGesture {
UIApplication.shared.resignFirstResponder()
}
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 2 years ago.
Improve this question
Although I found a couple tutorials for this online however this is as far as I could figure out.
HStack{
TextField("Placeholder", text: $text)
.keyboardType(.numberPad)
Button(action: {
self.hideKeyboard()
}, label: {
Image(systemName: "keyboard.chevron.compact.down")
})
}
#if canImport(UIKit)
extension View {
func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
#endif
Eventhough this works, I would prefer if I could put the button in a toolbar onto of the keypad which I can't figure out how to do.
if your main aim is to dismiss key board you can do the following
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
}
if you want to create a tool bar and manage your view so that when a textfield is clicked it doesn't occupy the space of textfield there is a very elegant cocoapod for that you just have to install it .its called IQKeyboardManager.
You just have to write one single line in didFinishLaunchingWithOptions method in AppDelegates .
IQKeyboardManager.shared.enable = true
Thats it now everything will be managed by it.
I'm using SwiftUI and I want to animate a view as soon as it appears (the explicit type of animation does not matter) for demo purposes in my app.
Let's say I just want to scale up my view and then scale it down to its original size again, I need to be able to animate the view to a new state and back to the original state right afterward.
Here's the sample code of what I've tried so far:
import SwiftUI
import Combine
struct ContentView: View {
#State private var shouldAnimate = false
private var scalingFactor: CGFloat = 2
var body: some View {
Text("hello world")
.scaleEffect(self.shouldAnimate ? self.scalingFactor : 1)
.onAppear {
let animation = Animation.spring().repeatCount(1, autoreverses: true)
withAnimation(animation) {
self.shouldAnimate.toggle()
}
}
}
Obviously this does not quite fulfill my requirements, because let animation = Animation.spring().repeatCount(1, autoreverses: true) only makes sure the animation (to the new state) is being repeated by using a smooth autoreverse = true setting, which still leads to a final state with the view being scaled to scalingFactor.
So neither can I find any property on the animation which lets my reverse my animation back to the original state automatically (without me having to interact with the view after the first animation), nor did I find anything on how to determine when the first animation has actually finished, in order to be able to trigger a new animation.
I find it pretty common practice to animate some View upon its appearance, e.g. just to showcase that this view can be interacted with, but ultimately not alter the state of the view. For example animate a bounce effect on a button, which in the end sets the button back to its original state. Of course I found several solutions suggesting to interact with the button to trigger a reverse animation back to its original state, but that's not what I'm looking for.
Here is a solution based on ReversingScale animatable modifier, from this my answer
Update: Xcode 13.4 / iOS 15.5
Complete test module is here
Tested with Xcode 11.4 / iOS 13.4
struct DemoReverseAnimation: View {
#State var scalingFactor: CGFloat = 1
var body: some View {
Text("hello world")
.modifier(ReversingScale(to: scalingFactor, onEnded: {
DispatchQueue.main.async {
self.scalingFactor = 1
}
}))
.animation(.default)
.onAppear {
self.scalingFactor = 2
}
}
}
Another approach which works if you define how long the animation should take:
struct ContentView: View {
#State private var shouldAnimate = false
private var scalingFactor: CGFloat = 2
var body: some View {
Text("hello world")
.scaleEffect(self.shouldAnimate ? self.scalingFactor : 1)
.onAppear {
let animation = Animation.easeInOut(duration: 2).repeatCount(1, autoreverses: true)
withAnimation(animation) {
self.shouldAnimate.toggle()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation(animation) {
self.shouldAnimate.toggle()
}
}
}
}
}
I'm looking for an equivalent of AppKit's NSHostingView for UIKit so that I can embed a SwiftUI view in UIKit. Unfortunately, UIKit does not have an equivalent class to NSHostingView. The closest we have as an equivalent of NSHostingController, named UIHostingController. Since a view controller contains a view, we should be able to call the appropriate UIViewController embedding methods, and then grab the view and use it directly.
There are many articles that explain that this is the way to embed a SwiftUI view inside UIKit. However, they typically fall short in explaining how you would communicate from UIKit ➡️ SwiftUI. For example, imagine I implemented a SwiftUI view that acts as a progress bar, periodically, I'd like the progress to be updated. I want my legacy/UIKit code to update the SwiftUI view to display the new progress.
The only article I found that came close to explaining how to manipulate an embedded view's content suggested we do so by using #ObservedObject:
import UIKit
import SwiftUI
import Combine
class CircleModel: ObservableObject {
var didChange = PassthroughSubject<Void, Never>()
var text: String { didSet { didChange.send() } }
init(text: String) {
self.text = text
}
}
struct CircleView : View {
#ObservedObject var model: CircleModel
var body: some View {
ZStack {
Circle()
.fill(Color.blue)
Text(model.text)
.foregroundColor(Color.white)
}
}
}
class ViewController: UIViewController {
private weak var timer: Timer?
private var model = CircleModel(text: "")
override func viewDidLoad() {
super.viewDidLoad()
addCircleView()
startTimer()
}
deinit {
timer?.invalidate()
}
}
private extension ViewController {
func addCircleView() {
let circleView = CircleView(model: model)
let controller = UIHostingController(rootView: circleView)
addChild(controller)
controller.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(controller.view)
controller.didMove(toParent: self)
NSLayoutConstraint.activate([
controller.view.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5),
controller.view.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.5),
controller.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
controller.view.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
func startTimer() {
var index = 0
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
index += 1
self?.model.text = "Tick \(index)"
}
}
}
This seems to make sense as the timer should trigger a chain of events that update the view:
✅ self?.model.text = "Tick 1" (In ViewController.startTimer()).
✅ didChange.send() (In CircleModel.text.didSet)
❌ Text(model.text) (In CircleView.body)
As you can see by the indicators (which specify if something was run or not), the problem is that didChange.send() never triggers a re-run of CircleView.body.
How do I communicate from UIKit > SwiftUI to manipulate a SwiftUI view that was embedded in UIKit?
All you need is to throw away that custom subject, and use standard #Published, as below
class CircleModel: ObservableObject {
#Published var text: String
init(text: String) {
self.text = text
}
}
Tested on: Xcode 11.2 / iOS 13.2
I'm trying to write a view that displays 3 buttons, I cannot get the animation to start on load.
When a button is tapped, I want it to animate until either:
it is tapped a second time
another of the 3 buttons is tapped
I have got the code working using a #Environment object to store the running state. It toggles between the 3 buttons nicely:
The code for this is here:
struct ContentView : View {
#EnvironmentObject var model : ModelClockToggle
var body: some View {
VStack {
ForEach(0...2) { timerButton in
ActivityBreak(myId: timerButton)
.padding()
}
}
}
}
import SwiftUI
struct ActivityBreak : View {
var myId: Int
#EnvironmentObject var model : ModelClockToggle
let anim1 = Animation.basic(duration: 1.0, curve: .easeInOut).repeatCount(Int.max)
let noAni = Animation.basic(duration: 0.2, curve: .easeInOut).repeatCount(0)
var body: some View {
return Circle()
.foregroundColor(.red)
.scaleEffect(self.model.amIRunning(clock: self.myId) ? 1.0 : 0.6)
.animation( self.model.amIRunning(clock: self.myId) ? anim1 : noAni )
.tapAction {
self.model.toggle(clock: self.myId)
}
}
}
For completeness, the model is:
import Foundation
import SwiftUI
import Combine
class ModelClockToggle: BindableObject {
let didChange = PassthroughSubject<ModelClockToggle, Never>()
private var clocksOn: [Bool] = [false,false,false]
init() {
clocksOn = []
clocksOn.append(UserDefaults.standard.bool(forKey: "toggle1"))
clocksOn.append(UserDefaults.standard.bool(forKey: "toggle2"))
clocksOn.append(UserDefaults.standard.bool(forKey: "toggle3"))
debugPrint(clocksOn)
}
func toggle(clock: Int) {
debugPrint(#function)
if clocksOn[clock] {
clocksOn[clock].toggle()
} else {
clocksOn = [false,false,false]
clocksOn[clock].toggle()
}
saveState()
didChange.send(self)
}
func amIRunning(clock: Int) -> Bool {
debugPrint(clocksOn)
return clocksOn[clock]
}
private func saveState() {
UserDefaults.standard.set(clocksOn[0], forKey: "toggle1")
UserDefaults.standard.set(clocksOn[1], forKey: "toggle2")
UserDefaults.standard.set(clocksOn[2], forKey: "toggle3")
}
}
How do I make the repeating animation start at load time based on the #Environment object I have passed into the View? Right now SwiftUI only seems to consider state change once the view is loaded.
I tried adding an .onAppear modifier, but that meant I had to use a different animator - which had very strange effects.
help gratefully received.
In your example, you are using an implicit animation. Those are animations that will look for changes on any animatable parameter such as size, position, opacity, color, etc. When SwiftUI detects any change, it will animate it.
In your specific case, Circles are normally scaled to 0.6 while not active, and 1.0 when active. Changes between inactive and active states, make your Circle to alter the scale, and this changes are animated in a loop.
However, your problem is that a Circle that is initially loaded at a 1.0 scale (because the model says it is active), will not detect a change: It starts at 1.0 and remains at 1.0. So there is nothing to animate.
In your comments you mention a solution, that involves having the model postpone loading the state of the Circle states. That way, your view is created first, then you ask the model to load states and then there is a change in your view that can be animated. That works, however, there is a problem with that.
You are making your model's behaviour dependent on the view. When it should really be the other way around. Suppose you have two instances of your view on the screen. Depending on timing, one will start fine, but the other will not.
The way to solve it, is making sure the entire logic is handle by the view itself. What you want to accomplish, is that your Circle always gets created with a scale of 0.6. Then, you check with the model to see if the Circel should be active. If so, you immediately change it to 1.0. This way you guarantee the view's animation.
Here is a possible solution, that uses a #State variable named booted to keep track of this. Your Circles will always be created with a scale of 0.6, but once the onAppear() method is call, the view will scale to 1.0 (if active), producing the corresponding animation.
struct ActivityBreak : View {
var myId: Int
#EnvironmentObject var model : ModelClockToggle
#State private var booted: Bool = false
// Beta 4
let anim1 = Animation.easeInOut(duration: 1.0).repeatCount(Int.max)
let noAni = Animation.easeInOut(duration: 0.2).repeatCount(0)
// Beta 3
// let anim1 = Animation.basic(duration: 1.0, curve: .easeInOut).repeatCount(Int.max)
// let noAni = Animation.basic(duration: 0.2, curve: .easeInOut).repeatCount(0)
var body: some View {
return Circle()
.foregroundColor(.red)
.scaleEffect(!booted ? 0.6 : self.model.amIRunning(clock: self.myId) ? 1.0 : 0.6)
.animation( self.model.amIRunning(clock: self.myId) ? anim1 : noAni )
.tapAction {
self.model.toggle(clock: self.myId)
}
.onAppear {
self.booted = true
}
}
}