I have a SwiftUI View (LandingScreenView.swift) that is using a button style implemented on another file (DefaultButtonStyle.swift). When I go to that Button Style file and change a line of code there, then return to the LandingScreenView.swift the SwiftUI preview has paused. It occurs always. This is a very fresh project with only those 2 files, I have one copy script on my build phases and I already set it to run "For install builds only". How can I make the SwiftUI works without pausing?
Xcode Version: 12.5
(I'm running it on an M1 MacBook, don't know if it makes any difference here)
import SwiftUI
struct LandingScreenView: View {
var body: some View {
VStack {
Spacer().frame(width: 0, height: 44, alignment: .center)
Text("app.title")
.font(.appTitle)
.frame(minWidth: 0,
maxWidth: .infinity,
alignment: .center)
Spacer()
VStack {
Button("landing.login.button", action: {
})
.buttonStyle(DefaultButton())
Button("landing.about.button", action: {
})
.buttonStyle(DefaultButton())
}
.frame(minWidth: 0, maxWidth: .infinity, alignment: .topLeading)
Spacer()
}.padding()
}
}
struct LandingScreenView_Previews: PreviewProvider {
static var previews: some View {
LandingScreenView()
}
}
import SwiftUI
struct DefaultButton: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
configuration.label
.padding()
.frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 44,
idealHeight: 44,
alignment: .center)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(40)
}
}
}
EDIT:
My problem here is not that the preview has to recompile it every time. What seems a little bit weird is that I have to press the resume button every time I go out the screen and return.
Related
I wanted some extra space on the top of the list so I tried using Spacer within the list and added modifiers to it. However I am not seeing the height getting reduced further. Below is the code for my view.
CustomView:
import SwiftUI
struct CustomView: View {
var body: some View {
VStack {
List {
Spacer()
.frame(minHeight: 1, idealHeight: 1, maxHeight: 2)
.fixedSize().listRowBackground(Color.clear)
UserLoginDetailsRowView().padding(.init(top: 0, leading: 5, bottom: 5, trailing: 5))
ForEach(1..<2) { _ in
VStack(alignment: .leading) {
Text("App version").fixedSize(horizontal: false, vertical: true).font(.headline).foregroundColor(.white)
Text("1.1.0").fixedSize(horizontal: false, vertical: true).font(.subheadline).foregroundColor(.white)
Spacer()
}.padding(.bottom, 15)
}.listRowBackground(Color.clear)
}
}.navigationBarTitle("Main Menu")
}
}
UserLoginDetailsRowView code:
import SwiftUI
struct UserLoginDetailsRowView: View {
var body: some View {
GeometryReader { geometry in
VStack(alignment: .center) {
Spacer()
Spacer()
Text("User's full name").lineLimit(2).font(.headline)
Text("Username").lineLimit(2).font(.subheadline)
Spacer()
}
ZStack {
Image("user-gray")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 30 , height: 30)
.offset(x: geometry.size.width / 2.8, y: -geometry.size.height/4)
}
}.frame(minHeight: 60.0)
}
}
This is how it looks with this code:
Regardless of the changes I make to minHeight, idealHeight and maxHeight in Spacer() within CustomView the result remains the same. However I want half of the space of what it's currently showing. I even tried replacing Spacer() with VStack and setting a frame height modifier to it, but at minimum, I do always see this much of space. I want the space reduced to half.
If I remove the Spacer() from CustomView then the image on my custom row gets chopped off and looks something like this. How do I reduce the space to half of what it is now?
Adding playground source code:
import SwiftUI
import PlaygroundSupport
struct CustomView: View {
var body: some View {
VStack {
Spacer().frame(minHeight: 25, idealHeight: 25, maxHeight: 30).fixedSize().listRowBackground(Color.clear)
List {
// Extra space for the top half of user icon within UserLoginDetailsRowView.
// Spacer().frame(minHeight: 25, idealHeight: 25, maxHeight: 30).fixedSize().listRowBackground(Color.clear)
UserLoginDetailsRowView().padding(.init(top: 0, leading: 5, bottom: 5, trailing: 5))
ForEach(1..<2) { _ in
VStack(alignment: .leading) {
Text("App Version").fixedSize(horizontal: false, vertical: true).font(.headline).foregroundColor(.white)
Text("1.1.0").fixedSize(horizontal: false, vertical: true).font(.subheadline).foregroundColor(.white)
Spacer()
}.padding(.bottom, 15)
}.listRowBackground(Color.clear)
}
}.navigationBarTitle("Back")
}
}
struct UserLoginDetailsRowView: View {
var body: some View {
GeometryReader { geometry in
VStack(alignment: .center) {
Spacer()
Spacer()
Text("User's full name").lineLimit(2).font(.headline)
Text("Username").lineLimit(2).font(.subheadline)
Spacer()
}
ZStack {
Image(systemName: "person.circle")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 22 , height: 22)
.offset(x: geometry.size.width / 2.8, y: -geometry.size.height/4)
}
}.frame(minHeight: 60.0)
}
}
PlaygroundPage.current.setLiveView(CustomView())
Solution
The primary gap comes from the list style itself. if you apply .listStyle(PlainListStyle()) to the List it will reduce it to what you are looking for.
List { ... }.listStyle(PlainListStyle())
If you want to further reduce it and control it to the last pixel apply a .onAppear modifier to the list and set the content inset to your desired value.
List { .... }.onAppear(perform: {
UITableView.appearance().contentInset.top = -60
})
In the above code the value 60 is arbitrary in nature and you need to play around to get a value that fits your UI.
Explanation
The List default style adds a larger header which creates the spacing you were having issues with, this behaviour is similar to GroupedListStyle. From the documentation
On iOS, the grouped list style displays a larger header and footer than the plain style, which visually distances the members of different sections.
You can play around with other List Styles from the documentation to fit your needs better.
Full Playground Code - For .onAppear solution
import SwiftUI
import PlaygroundSupport
struct CustomView: View {
var body: some View {
VStack(alignment:.leading, spacing:0) {
List {
// Extra space for the top half of user icon within UserLoginDetailsRowView.
Spacer().frame(minHeight: 1, idealHeight: 1, maxHeight: 2)
.fixedSize().listRowBackground(Color.clear)
UserLoginDetailsRowView().padding(.init(top: 0, leading: 5, bottom: 5, trailing: 5))
ForEach(1..<2) { _ in
VStack(alignment: .leading) {
Text("App Version").fixedSize(horizontal: false, vertical: true).font(.headline).foregroundColor(.white)
Text("1.1.0").fixedSize(horizontal: false, vertical: true).font(.subheadline).foregroundColor(.white)
Spacer()
}.padding(.bottom, 15)
}.listRowBackground(Color.clear)
}.onAppear(perform: {
UITableView.appearance().contentInset.top = -60
})
}.navigationBarTitle("Back")
}
}
struct UserLoginDetailsRowView: View {
var body: some View {
GeometryReader { geometry in
VStack(alignment: .center) {
Spacer()
Spacer()
Text("User's full name").lineLimit(2).font(.headline)
Text("Username").lineLimit(2).font(.subheadline)
Spacer()
}
ZStack {
Image(systemName: "person.circle")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 22 , height: 22)
.offset(x: geometry.size.width / 2.8, y: -geometry.size.height/4)
}
}.frame(minHeight: 60.0)
}
}
PlaygroundPage.current.setLiveView(CustomView())
Working on all things accessibility related and found that .accessibilityHidden(true) doesn't seem to work correctly. Even though VO doesn't read anything, it still shows the white box as you swipe around the page. The only way I've solved this is to put a VStack around the entire thing and then it seems to be fine, however it truncates the welcomeBodyText in simulator but not on a real device, which is worrying because I need to ensure that this works across all devices.
As the HeaderNav just shows the logo and company name, there is no reason to have it selectable, nor read out on every single page, so I wanted to completely hide it from VO and immediately jump to reading the welcomeHeaderText followed by the welcomeBodyText.
HeaderNav.swift
import SwiftUI
struct HeaderNav: View {
var body: some View {
VStack {
Spacer()
.frame(minHeight: 26, idealHeight: 26, maxHeight: 26)
.fixedSize()
HStack(spacing: 16) {
Spacer()
Image(decorative: "LogoHeader")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 70)
Text(LocalizationStrings.companyName)
.fontWeight(.light)
.foregroundColor(Color("WhiteText"))
.font(.system(size: 34))
.tracking(0.8)
Spacer()
} // End HStack
.accessibilityElement(children: .ignore)
.accessibilityLabel(LocalizationStrings.companyName)
} // End VStack
}
}
WelcomeTextView.swift
import SwiftUI
struct WelcomeTextView: View {
var body: some View {
Section {
VStack { // This is what needed to be added to make it work but causes issues in the simulator
HeaderNav()
.accessibilityHidden(true)
VStack(spacing: 10) {
Group {
HeaderText(text: LocalizationStrings.welcomeHeaderText)
BodyText(text: LocalizationStrings.welcomeBodyText)
} // End Group (Page Description)
.pageDescr()
} // End VStack
} // End VStack to make it work
.accessibilityElement(children: .combine)
} // End Section
.listRowBackground(Color("mainbkg"))
.listRowSeparator(.hidden)
}
}
pageDescr is a View Modifier that looks like:
struct PageDescr: ViewModifier {
func body(content: Content) -> some View {
VStack(alignment: .leading, spacing: 20) {
content
}
.padding(EdgeInsets(top: 5, leading: 32.0, bottom: 25, trailing: 32.0))
}
}
It was the top padding that was causing the issue, so I used a fixed size spacer in the HeaderNav above the closing } of the VStack instead and set the top padding to 0. Now it works.
Like this:
Spacer()
.frame(minHeight: 32, idealHeight: 32, maxHeight: 32)
.fixedSize()
I am trying to adapt the font size of a text element whose length varies greatly so that it fills the whole frame where it is located and no word needs to be truncated (in a iOS app). In the screenshot below, I would like the sentence to reach the buttons at the bottom of the screen.
Please find below the structure of my code. I have seen a few threads on SO dealing with this issue, but I have not been able to replicate the solution, because of my poor skills in Swift... If someone were so kind as to give me a detailed explanation of what lines of code should be inserted and where to place them, I'd be very grateful!
I have tried to adjust the font size so to say manually, by incrementing or decrementing it via a function according to the length of the string that Text receives as its argument, but it only yielded a mess, because long words need their own separate line, which mars my computation.
With thanks,
Julien
import SwiftUI
import AVKit
import UIKit
import AVFoundation
import Foundation
import CoreServices
import CoreData
import MediaPlayer
struct ContentView: View {
// Various functions
#State var audioPlayer: AVAudioPlayer!
var body: some View {
VStack {
var currentSentence = "string of variable length, could be a long or a very short sentence"
Text(currentSentence) // the variable currentSentence contains a string whose length varies greatly
.font(.system(size: 40, weight: .light, design: .serif))
HStack {
Spacer()
// Play button
Button(action: {
// Action
}) {
Image(systemName: "play.square.fill").resizable()
.frame(width: 150.0, height: 200.0, alignment: .bottom)
}
.frame(maxHeight: .infinity, alignment: .bottom)
Spacer()
// Pause button
Button(action: {
// Action
}){
Image(systemName: "square.slash").resizable()
.frame(width: 150.0, height: 200.0)
.aspectRatio(contentMode: .fill)
}
.frame(maxHeight: .infinity, alignment: .bottom)
Spacer()
}
}
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You need to use .minimumScaleFactor on the text and set the text font to the maximum.
struct ContentView: View {
var body: some View {
VStack (spacing: 0) {
var currentSentence = "string of variable length, could be a long or a very short sentence"
Text(currentSentence)
.font(.system(size: 100, weight: .light, design: .serif))
.minimumScaleFactor(0.1)
Spacer()
HStack (alignment: .bottom, spacing: 0) {
Spacer()
// Play button
Button(action: {
// Action
}) {
Image(systemName: "play.square.fill").resizable()
.frame(width: 150.0, height: 200.0, alignment: .bottom)
}
Spacer()
// Pause button
Button(action: {
// Action
}){
Image(systemName: "square.slash").resizable()
.frame(width: 150.0, height: 200.0)
.aspectRatio(contentMode: .fill)
}
Spacer()
}
}
.edgesIgnoringSafeArea([.bottom])
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
}
}
}
In some cases there might be little bit space between text and buttons, It means new line with bigger font can't fit there.
I have a ScrollView with multiple Buttons. A Button contains a Image and a Text underneath.
As the images are pretty large I am using .scaledToFill and .clipped. And it seems that the 'clipped' part of the image is still clickable even if it's not shown.
In the video you see I am clicking on button 1 but button 2 is triggered.
This is my Coding. The Image is inside the View Card.
struct ContentView: View {
#State var useWebImage = false
#State var isSheetShowing = false
#State var selectedIndex = 0
private let images = [
"https://images.unsplash.com/photo-1478368499690-1316c519df07?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2706&q=80",
"https://images.unsplash.com/photo-1507154258-c81e5cca5931?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2600&q=80",
"https://images.unsplash.com/photo-1513310719763-d43889d6fc95?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2734&q=80",
"https://images.unsplash.com/photo-1585766765962-28aa4c7d719c?ixlib=rb-1.2.1&auto=format&fit=crop&w=2734&q=80",
"https://images.unsplash.com/photo-1485970671356-ff9156bd4a98?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2734&q=80",
"https://images.unsplash.com/photo-1585607666104-4d5b201d6d8c?ixlib=rb-1.2.1&auto=format&fit=crop&w=2700&q=80",
"https://images.unsplash.com/photo-1577702066866-6c8897d06443?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2177&q=80",
"https://images.unsplash.com/photo-1513809491260-0e192158ae44?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2736&q=80",
"https://images.unsplash.com/photo-1582092723055-ad941d1db0d4?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2700&q=80",
"https://images.unsplash.com/photo-1478264635837-66efba4b74ba?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjF9&auto=format&fit=crop&w=2682&q=80"
]
var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 40) {
Text(useWebImage ? "WebImage is used." : "SwiftUI Image is used")
.font(.system(size: 18))
.bold()
.kerning(0.5)
.padding(.top, 20)
Toggle(isOn: $useWebImage) {
Text("Use WebImage")
.font(.system(size: 18))
.bold()
.kerning(0.5)
.padding(.top, 20)
}
ForEach(0..<images.count) { index in
Button(action: {
self.selectedIndex = index
self.isSheetShowing.toggle()
}) {
Card(imageUrl: self.images[index], index: index, useWebImage: self.$useWebImage)
}
.buttonStyle(PlainButtonStyle())
}
}
.padding(.horizontal, 20)
.sheet(isPresented: self.$isSheetShowing) {
DestinationView(imageUrl: self.images[self.selectedIndex], index: self.selectedIndex, useWebImage: self.$useWebImage)
}
}
.navigationBarTitle("Images")
}
}
}
struct Card: View {
let imageUrl: String
let index: Int
#Binding var useWebImage: Bool
var body: some View {
VStack {
if useWebImage {
WebImage(url: URL(string: imageUrl))
.resizable()
.indicator(.activity)
.animation(.easeInOut(duration: 0.5))
.transition(.fade)
.scaledToFill()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 250, maxHeight: 250, alignment: .center)
.cornerRadius(12)
.clipped()
} else {
Image("image\(index)")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 250, maxHeight: 250, alignment: .center)
.cornerRadius(12)
.clipped()
}
HStack {
Text("Image #\(index + 1) (\(useWebImage ? "WebImage" : "SwiftUI Image"))")
.font(.system(size: 18))
.bold()
.kerning(0.5)
Spacer()
}
}
.padding(2)
.border(Color(.systemRed), width: 2)
}
}
Do you have an idea how to fix this issue?
I already tried to use .resizable(resizingMode: .tile) but I need to shrink the image before I could use just a tile.
For detailed information you can also find the project on GitHub GitHub Project
I would appreciate your help a lot.
The .clipped affects only drawing, and by-default Button has all content clickable not depending what it is.
So if you want make your button clickable only in image area, you have to limit hit testing only to its rect explicitly and disable everything else.
Here is a demo of possible approach. Tested with Xcode 11.4 / iOS 13.4.
Demo code (simplified variant of your snapshot):
struct ButtonCard: View {
var body: some View {
VStack {
Image("sea")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 250, maxHeight: 250, alignment: .center)
.cornerRadius(12)
.contentShape(Rectangle()) // << define clickable rect !!
.clipped()
HStack {
Text("Image #1")
.font(.system(size: 18))
.bold()
.kerning(0.5)
Spacer()
}.allowsHitTesting(false) // << disable label area !!
}
.padding(2)
.border(Color(.systemRed), width: 2)
}
}
struct TestClippedButton: View {
var body: some View {
Button(action: { print(">> tapped") }) {
ButtonCard()
}.buttonStyle(PlainButtonStyle())
}
}
I'd like to ask you some help. I'm trying to capture VStack height using geometry and then based on that VStack height value, calculate its child element's height (inside VStack).
Image of my current view of VStack
I used .frame outside of VStack to fill the whole screen. Then I used .border to visually check if it actually fills the screen (it works fine)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: Alignment.topLeading)
The problem can be seen inside the VStack, the height is displayed as GCFloat 734. Even though the border size is a lot bigger.
struct BodyView: View {
#State var stackHeight : CGFloat = 0
var body: some View {
GeometryReader { geometry in
VStack {
Text("VStack size: \(self.stackHeight)")
.bold()
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
.background(Color.green)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: Alignment.topLeading)
.onAppear{
self.stackHeight = geometry.size.height
}
.border(Color.purple, width: 5)
}
}
}
struct BodyView_Previews: PreviewProvider {
static var previews: some View {
BodyView()
}
}
How could I capture the actual size of the VStack when it finishes loading?
When I trigger (onAppear) the VStack height appears correct, however I need it to be captured instantly.
If I understand your questions, you're very close — you just need to use to GeometryProxy type:
import SwiftUI
struct BodyView: View {
var body: some View {
GeometryReader { geometry in
VStack {
Text("VStack size: \(geometry.size.height)") // no need for state var anymore
.bold()
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
.background(Color.green)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: Alignment.topLeading)
.border(Color.purple, width: 5)
}
}
}
struct BodyView_Previews: PreviewProvider {
static var previews: some View {
BodyView()
}
}
A GeometryReader will lay itself out again if its size changes.