I have a TabView with six pages. The animation is in the last page.
When I attend to the last page the animation shows for a split second and disappear completely.
Thought it might be problem with the animation but it works elsewhere just fine.
I present this TabView using sheet.
Last page:
struct SixScreen: View{
#EnvironmentObject var session: SessionStore
#Binding var dismiss: Bool
var body: some View{
VStack(spacing: 16){
Spacer()
LottieView(name: "complete")
.frame(width: 200, height: 200, alignment: .center)
Button(action: {
dismiss.toggle()
}, label: {
Text("Start")
.frame(width: 100, height: 50, alignment: .center)
.foregroundColor(.blue)
.background(Color.white)
.cornerRadius(10)
.shadow(color: .blue, radius: 5, x: 0, y: 1)
})
.padding(.bottom, 32)
Spacer()
}
}
}
Lottie View implementation:
struct LottieView: UIViewRepresentable {
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
var name: String!
var animationView = AnimationView()
class Coordinator: NSObject {
var parent: LottieView
init(_ animationView: LottieView) {
self.parent = animationView
super.init()
}
}
func makeUIView(context: UIViewRepresentableContext<LottieView>) -> UIView {
let view = UIView()
animationView.animation = Animation.named(name)
animationView.contentMode = .scaleAspectFit
animationView.loopMode = .loop
animationView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(animationView)
NSLayoutConstraint.activate([
animationView.widthAnchor.constraint(equalTo: view.widthAnchor),
animationView.heightAnchor.constraint(equalTo: view.heightAnchor)
])
return view
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<LottieView>) {
animationView.play()
}
}
Tab View:
Group{
TabView{
FirstScreen()
SecondScreen()
ThirdScreen()
FourthScreen()
FifthScreen()
SixScreen(dismiss: $dismiss)
}
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
.padding(.bottom)
}
.background(gradient)
.edgesIgnoringSafeArea(.all)
.navigationBarHidden(true)
}
Try to force refresh the view using the .id() modifier
struct AnimationView: View {
#State private var lottieID = UUID()
var body: some View {
LottieView(name: "complete")
.frame(width: 200, height: 200, alignment: .center)
.id(lottieID)
.onAppear {
lottieID = UUID()
}
}
}
After a lot of research and testing, if you're looking to keep the Lottie animation live inside the SwiftUI TabView you should add this code snippet:
public func makeUIView(context: UIViewRepresentableContext<LottieView>) -> UIView {
let view = UIView(frame: .zero)
let animation = Animation.named(lottieFile)
animationView.animation = animation
animationView.animationSpeed = animationSpeed
animationView.contentMode = .scaleAspectFit
animationView.loopMode = loopMode
animationView.backgroundBehavior = .pauseAndRestore <------
Related
I am trying to pass a #Binding UI Image from an Image picker to another view through a sheet. I tried to use the OnDismiss() parameter but it did not work, even tried ondisapper(). What i want is that, when i press a button, it should open up the image picker directly. When a user has selected an image it should take that image and navigate to another view and show the image. This is the code im trying but is not working : -
NavigationBar.swift
import SwiftUI
struct NavigationBar: View {
var title = ""
#Binding var hasScrolled: Bool
#State var showUploadPost = false
#State var imagePickerPresented = false
#State private var selectedImage: UIImage?
#State var postImage: Image?
// #State var showAccount = false
#State var captionText = ""
var body: some View {
ZStack {
Color.clear
.background(.ultraThinMaterial)
.blur(radius: 10)
.opacity(hasScrolled ? 1 : 0)
Text(title)
.animatableFont(size: hasScrolled ? 22 : 34, weight: .bold)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, 20)
.padding(.top, 20)
.offset(y: hasScrolled ? -4 : 0)
HStack(spacing: 16) {
Button {
imagePickerPresented = true
} label: {
Image(systemName: "plus.square")
.font(.title3.weight(.bold))
.frame(width: 46, height: 46)
.foregroundColor(.secondary)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 14, style: .continuous))
.strokeStyle(cornerRadius: 14)
}
.sheet(isPresented: $imagePickerPresented, onDismiss: {
newPostView(image: $selectedImage) // this is the problem
}) {
ImagePicker(image: $selectedImage)
}
.frame(maxWidth: .infinity, alignment: .trailing)
.padding(.trailing, 20)
.padding(.top, 20)
.offset(y: hasScrolled ? -4 : 0)
}
.frame(height: hasScrolled ? 44 : 70)
.frame(maxHeight: .infinity, alignment: .top)
}
}
extension NavigationBar {
func loadImage() {
guard let selectedImage = selectedImage else { return }
postImage = Image(uiImage: selectedImage)
}
}
newPostView.swift
import SwiftUI
struct newPostView: View {
#Binding var image: UIImage?
var body: some View {
VStack {
Text("hi")
}
}
}
ImagePicker.swift
struct ImagePicker: UIViewControllerRepresentable {
#Binding var image: UIImage?
#Environment(\.presentationMode) var mode
func makeUIViewController(context: Context) -> some UIViewController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
return picker
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let image = info[.originalImage] as? UIImage else { return }
self.parent.image = image
self.parent.mode.wrappedValue.dismiss()
}
}
}
Solution 1:
Simply use hidden navigation bar technique to push it to another view on selecting the Image.
struct NavigationBar: View {
#State var imagePickerPresented = false
#State private var selectedImage: UIImage?
#State private var newPost = false
var body: some View {
VStack {
Text("Hello, World!")
Button {
imagePickerPresented = true
} label: {
Image(systemName: "plus.square")
.font(.title3.weight(.bold))
.frame(width: 46, height: 46)
.foregroundColor(.secondary)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 14, style: .continuous))
}
.sheet(isPresented: $imagePickerPresented, onDismiss: {
newPost.toggle() //1 <--- FIX
}) {
ImagePicker(image: $selectedImage)
}
// 2 FIX ⬇️
// Hide Navigation Link
NavigationLink("NewPostView", isActive: $newPost) {
newPostView(image: $selectedImage)
}.hidden()
}
}
}
Note: This works when the NavigationBar is inside the NavigationView.
For example -
struct ContenView: View {
var body: some View {
NavigationView{
NavigationBar()
}
}
}
Solution 2: If you don't want Navigation to the newPostView, use like this
struct NavigationBar: View {
#State var imagePickerPresented = false
#State private var selectedImage: UIImage?
#State private var newPost = false
var body: some View {
VStack {
Text("Hello, World!")
Button {
imagePickerPresented = true
} label: {
Image(systemName: "plus.square")
.font(.title3.weight(.bold))
.frame(width: 46, height: 46)
.foregroundColor(.secondary)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 14, style: .continuous))
}
.sheet(isPresented: $imagePickerPresented, onDismiss: {
newPost.toggle() //1 <--- FIX
}) {
ImagePicker(image: $selectedImage)
}
.fullScreenCover(
isPresented: $newPost) {
newPostView(image: $selectedImage)
}
}
}
}
struct ContentView: View {
#State var imagePickerPresented = false
#State private var selectedImage: UIImage?
#State private var newPost = false
var body: some View {
NavigationBar()
}
}
I want the image to not have its own background.
But I don’t understand where to set .background or any other way so that it doesn’t stand out
struct CustomDatePicker: View {
#State var date = Date()
let finalDate = Date.now.addingTimeInterval(604800)
var body: some View {
VStack {
ZStack {
DatePicker("label", selection: $date, in: Date.now...finalDate, displayedComponents: .date)
.datePickerStyle(CompactDatePickerStyle())
.labelsHidden()
Image(systemName: "calendar")
.font(.system(size: 21))
.foregroundColor(titleFieldColor)
.userInteractionDisabled()
}
.background(.clear)
}
}
}
struct NoHitTesting: ViewModifier {
func body(content: Content) -> some View {
SwiftUIWrapper { content }.allowsHitTesting(false)
}
}
struct SwiftUIWrapper<T: View>: UIViewControllerRepresentable {
let content: () -> T
func makeUIViewController(context: Context) -> UIHostingController<T> {
UIHostingController(rootView: content())
}
func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {}
}
extension View {
func userInteractionDisabled() -> some View {
self.modifier(NoHitTesting())
}
}
And here full string.
var body: some View {
HStack {
Text(title.count > 0 ? title : nameOfCategory.localized(LocalizationService.shared.language))
.frame(width: 200, height: 40, alignment: .leading)
.font(.system(size: 16))
.foregroundColor(titleFieldColor)
CustomDatePicker()
.background(backgroundFieldColor.opacity(1))
.frame(width: 30, height: 30)
}
.frame(width: 280, height: 40)
.background(backgroundFieldColor)
.clipShape(RoundedRectangle(cornerRadius: 25))
}
I have tried many different options but none of the options work for this line:
I can't reduce the frame of this CustomDatePicker
I made .background the same color as view, did oppacity - 1, tried background(.clear)
This white zone doesn't change at all
How else can you try to fix this?
iOS 14.4 + Xcode 12.4
I want to make a simple checklist in SwiftUI on iOS where the text for each item is a TextEditor.
First, I create the basic app structure and populate it with some demo content:
import SwiftUI
#main
struct TestApp: App {
#State var alpha = "Alpha"
#State var bravo = "Bravo is a really long one that should wrap to multiple lines."
#State var charlie = "Charlie"
init(){
//Remove the default background of the TextEditor/UITextView
UITextView.appearance().backgroundColor = .clear
}
var body: some Scene {
WindowGroup {
ScrollView{
VStack(spacing: 7){
TaskView(text: $alpha)
TaskView(text: $bravo)
TaskView(text: $charlie)
}
.padding(20)
}
.background(Color.gray)
}
}
}
Then each TaskView represents a task (the white box) in the list:
struct TaskView:View{
#Binding var text:String
var body: some View{
HStack(alignment:.top, spacing:8){
Button(action: {
print("Test")
}){
Circle()
.strokeBorder(Color.gray,lineWidth: 1)
.background(Circle().foregroundColor(Color.white))
.frame(width:22, height: 22)
}
.buttonStyle(PlainButtonStyle())
FieldView(name: $text)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(EdgeInsets(top:10, leading:10, bottom: 10, trailing: 30))
.background(Color.white)
.cornerRadius(5)
}
}
Then finally, each of the TextEditors is in a FieldView like this:
struct FieldView: View{
#Binding var name: String
var body: some View{
ZStack{
Text(name)
.padding(EdgeInsets(top: -7, leading: -3, bottom: -5, trailing: -3))
.opacity(0)
TextEditor(text: $name)
.fixedSize(horizontal: false, vertical: true)
.padding(EdgeInsets(top: -7, leading: -3, bottom: -5, trailing: -3))
}
}
}
As you can see in the screenshot above, the initial height of the TextEditor doesn't automatically size to fit the text. But as soon as I type in it, it resizes appropriately. Here's a video that shows that:
How can I get the view to have the correct initial height? Before I type in it, the TextEditor scrolls vertically so it seems to have the wrong intrinsic content size.
Note: views are left semi-transparent with borders so you can see/debug what's going on.
struct FieldView: View{
#Binding var name: String
#State private var textEditorHeight : CGFloat = 100
var body: some View{
ZStack(alignment: .topLeading) {
Text(name)
.background(GeometryReader {
Color.clear
.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)
})
//.opacity(0)
.border(Color.pink)
.foregroundColor(Color.red)
TextEditor(text: $name)
.padding(EdgeInsets(top: -7, leading: -3, bottom: -5, trailing: -7))
.frame(height: textEditorHeight + 12)
.border(Color.green)
.opacity(0.4)
}
.onPreferenceChange(ViewHeightKey.self) { textEditorHeight = $0 }
}
}
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
print("Reporting height: \(value)")
}
}
First, I used a PreferenceKey to pass the height from the "invisible" text view back up the view hierarchy. Then, I set the height of the TextEditor frame with that value.
Note that the view is now aligned to topLeading -- in your initial example, the invisible text was center aligned.
One thing I'm not crazy about is the use of the edge insets -- these feel like magic numbers (well, they are...) and I'd rather have a solution without them that still kept the Text and TextEditor completely aligned. But, this works for now.
Update, using UIViewRepresentable with UITextView
This seems to work and avoid the scrolling problems:
struct TaskView:View{
#Binding var text:String
#State private var textHeight : CGFloat = 40
var body: some View{
HStack(alignment:.top, spacing:8){
Button(action: {
print("Test")
}){
Circle()
.strokeBorder(Color.gray,lineWidth: 1)
.background(Circle().foregroundColor(Color.white))
.frame(width:22, height: 22)
}
.buttonStyle(PlainButtonStyle())
FieldView(text: $text, heightToTransmit: $textHeight)
.frame(height: textHeight)
.border(Color.red)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(EdgeInsets(top:10, leading:10, bottom: 10, trailing: 30))
.background(Color.white)
.cornerRadius(5)
}
}
struct FieldView : UIViewRepresentable {
#Binding var text : String
#Binding var heightToTransmit: CGFloat
func makeUIView(context: Context) -> UIView {
let view = UIView()
let textView = UITextView(frame: .zero, textContainer: nil)
textView.delegate = context.coordinator
textView.backgroundColor = .yellow // visual debugging
textView.isScrollEnabled = false // causes expanding height
context.coordinator.textView = textView
textView.text = text
view.addSubview(textView)
// Auto Layout
textView.translatesAutoresizingMaskIntoConstraints = false
let safeArea = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
textView.topAnchor.constraint(equalTo: safeArea.topAnchor),
textView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
textView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor)
])
return view
}
func updateUIView(_ view: UIView, context: Context) {
context.coordinator.heightBinding = $heightToTransmit
context.coordinator.textBinding = $text
DispatchQueue.main.async {
context.coordinator.runSizing()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
class Coordinator : NSObject, UITextViewDelegate {
var textBinding : Binding<String>?
var heightBinding : Binding<CGFloat>?
var textView : UITextView?
func runSizing() {
guard let textView = textView else { return }
textView.sizeToFit()
self.textBinding?.wrappedValue = textView.text
self.heightBinding?.wrappedValue = textView.frame.size.height
}
func textViewDidChange(_ textView: UITextView) {
runSizing()
}
}
}
My UIImagePickerController has a white Cancel button for some reason, making it very hard to see...
Here is my ImagePicker class:
struct ImagePicker: UIViewControllerRepresentable {
let key: String
#Binding var uiImage: UIImage?
#Environment(\.presentationMode) var presentationMode
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIImagePickerController {
let controller = UIImagePickerController()
controller.navigationBar.topItem?.rightBarButtonItem?.tintColor = UIColor.orange
controller.navigationBar.topItem?.leftBarButtonItem?.tintColor = UIColor.orange
controller.navigationBar.backItem?.leftBarButtonItem?.tintColor = UIColor.orange
controller.sourceType = .photoLibrary
controller.delegate = context.coordinator
return controller
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate{
var parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let img = info[.originalImage] as? UIImage {
parent.uiImage = img
}
parent.presentationMode.wrappedValue.dismiss()
}
}
}
struct Dialog: View {
#Binding var dialogKey: String?
#Binding var showImagePicker: Bool
#Binding var changed: Bool
#Binding var uiImage: UIImage?
#Binding var loadingKey: String?
#State var noImage = false
var body: some View {
HStack(spacing: 0) {
Button(action: { self.showImagePicker.toggle() }){
Text(noImage ? "Add" : "Replace")
.frame(width: 80)
.multilineTextAlignment(.center)
.font(.custom("Seravek-Bold", size: 15))
.foregroundColor(Color.white)
.padding(.vertical, 15)
.background(
Rectangle()
.fill(Color("Meeq"))
)
.cornerRadius(radius: 5, corners: [.topLeft, .bottomLeft])
.shadow(color: Color.black.opacity(0.5), radius: 1, x: 0, y: 1)
}
Button(action: { self.deleteImage() }){
Text("Delete")
.frame(width: 80)
.multilineTextAlignment(.center)
.font(.custom("Seravek-Bold", size: 15))
.padding(.vertical, 15)
.foregroundColor(Color.white)
.background(
Rectangle()
.fill(Color("Exit"))
)
.shadow(color: Color.black.opacity(0.5), radius: 1, x: 0, y: 1)
.opacity(noImage ? 0.3 : 1.0)
}
.disabled(noImage)
.onAppear {
// Check if there's no image for the selected circle
if let dialogKey = dialogKey {
let key = UserDefaults.standard.string(forKey: dialogKey)
if key == nil { noImage = true }
print("Delete onAppear: \(key)")
}
}
Button(action: { self.dialogKey = nil }){
Text("Cancel")
.frame(width: 80)
.multilineTextAlignment(.center)
.font(.custom("Seravek-Bold", size: 15))
.foregroundColor(Color.white)
.padding(.vertical, 15)
.background(
Rectangle()
.fill(Color.gray)
)
.cornerRadius(radius: 5, corners: [.topRight, .bottomRight])
.shadow(color: Color.black.opacity(0.5), radius: 1, x: 1, y: 1)
}
}
.sheet(isPresented: $showImagePicker, onDismiss: loadImage){
if let key = dialogKey {
ImagePicker(key: key, uiImage: self.$uiImage)
.accentColor(Color.orange)
}
}
}
}
As you can see I attempted to change the Cancel button color to orange with the above code:
controller.navigationBar.topItem?.rightBarButtonItem?.tintColor = UIColor.orange
controller.navigationBar.topItem?.leftBarButtonItem?.tintColor = UIColor.orange
controller.navigationBar.backItem?.leftBarButtonItem?.tintColor = UIColor.orange
however it is still white.
Any idea how I can turn the Cancel button orange?
I'm trying to recreate the iOS 11/12 App Store with SwiftUI.
Let's imagine the "story" is the view displayed when tapping on the card.
I've done the cards, but the problem I'm having now is how to do the animation done to display the "story".
As I'm not good at explaining, here you have a gif:
Gif 1
Gif 2
I've thought of making the whole card a PresentationLink, but the "story" is displayed as a modal, so it doesn't cover the whole screen and doesn't do the animation I want.
The most similar thing would be NavigationLink, but that then obliges me to add a NavigationView, and the card is displayed like another page.
I actually do not care whether its a PresentationLink or NavigationLink or whatever as long as it does the animation and displays the "story".
Thanks in advance.
My code:
Card.swift
struct Card: View {
var icon: UIImage = UIImage(named: "flappy")!
var cardTitle: String = "Welcome to \nCards!"
var cardSubtitle: String = ""
var itemTitle: String = "Flappy Bird"
var itemSubtitle: String = "Flap That!"
var cardCategory: String = ""
var textColor: UIColor = UIColor.white
var background: String = ""
var titleColor: Color = .black
var backgroundColor: Color = .white
var body: some View {
VStack {
if background != "" {
Image(background)
.resizable()
.frame(width: 380, height: 400)
.cornerRadius(20)
} else {
RoundedRectangle(cornerRadius: 20)
.frame(width: 400, height: 400)
.foregroundColor(backgroundColor)
}
VStack {
HStack {
VStack(alignment: .leading) {
if cardCategory != "" {
Text(verbatim: cardCategory.uppercased())
.font(.headline)
.fontWeight(.heavy)
.opacity(0.3)
.foregroundColor(titleColor)
//.opacity(1)
}
HStack {
Text(verbatim: cardTitle)
.font(.largeTitle)
.fontWeight(.heavy)
.lineLimit(3)
.foregroundColor(titleColor)
}
}
Spacer()
}.offset(y: -390)
.padding(.bottom, -390)
HStack {
if cardSubtitle != "" {
Text(verbatim: cardSubtitle)
.font(.system(size: 17))
.foregroundColor(titleColor)
}
Spacer()
}
.offset(y: -50)
.padding(.bottom, -50)
}
.padding(.leading)
}.padding(.leading).padding(.trailing)
}
}
So
Card(cardSubtitle: "Welcome to this library I made :p", cardCategory: "CONNECT", background: "flBackground", titleColor: .white)
displays:
SwiftUI doesn't do custom modal transitions right now, so we have to use a workaround.
One method that I could think of is to do the presentation yourself using a ZStack. The source frame could be obtained using a GeometryReader. Then, the destination shape could be controlled using frame and position modifiers.
In the beginning, the destination will be set to exactly match position and size of the source. Then immediately afterwards, the destination will be set to fullscreen size in an animation block.
struct ContentView: View {
#State var isPresenting = false
#State var isFullscreen = false
#State var sourceRect: CGRect? = nil
var body: some View {
ZStack {
GeometryReader { proxy in
Button(action: {
self.isFullscreen = false
self.isPresenting = true
self.sourceRect = proxy.frame(in: .global)
}) { ... }
}
if isPresenting {
GeometryReader { proxy in
ModalView()
.frame(
width: self.isFullscreen ? nil : self.sourceRect?.width ?? nil,
height: self.isFullscreen ? nil : self.sourceRect?.height ?? nil)
.position(
self.isFullscreen ? proxy.frame(in: .global).center :
self.sourceRect?.center ?? proxy.frame(in: .global).center)
.onAppear {
withAnimation {
self.isFullscreen = true
}
}
}
}
}
.edgesIgnoringSafeArea(.all)
}
}
extension CGRect {
var center : CGPoint {
return CGPoint(x:self.midX, y:self.midY)
}
}
SwiftUI in iOS/tvOS 14 and macOS 11 has matchedGeometryEffect(id:in:properties:anchor:isSource:) to animate view transitions between different hierarchies.
Link to Official Documentation
Here's a minimal example:
struct SomeView: View {
#State var isPresented = false
#Namespace var namespace
var body: some View {
VStack {
Button(action: {
withAnimation {
self.isPresented.toggle()
}
}) {
Text("Toggle")
}
SomeSourceContainer {
MatchedView()
.matchedGeometryEffect(id: "UniqueViewID", in: namespace, properties: .frame, isSource: !isPresented)
}
if isPresented {
SomeTargetContainer {
MatchedTargetView()
.matchedGeometryEffect(id: "UniqueViewID", in: namespace, properties: .frame, isSource: isPresented)
}
}
}
}
}