My SwiftUI Interface looks different on each iPhone model. I want to ask for best practice of creating scalable(or at least adaptable) interface with SwiftUI.
For example:
struct NewStruct: View {
#State var someState = false
var body: some View {
VStack {
HStack {
Spacer().frame(width: 25)
Text("Text1")
Spacer()
}
Button(action: {
someState = !someState
}, label: {
ZStack {
RoundedRectangle(cornerRadius: 15).fill(Color.white).frame(width: 350, height: 60)
HStack {
HStack {
Spacer().frame(maxWidth: 10)
Image(systemName: "bolt.fill").resizable().scaledToFit().frame(width: 35, height: 35).cornerRadius(30).padding()
Text("Text1").foregroundColor(Color.black)
Spacer()
Spacer(minLength: 30)
Image(systemName: "poweroff").resizable().frame(width: 25, height: 25)
Image(systemName: "power").resizable().frame(width: 25, height: 25)
Spacer().frame(maxWidth: 30)
}
}
}
})
}
}
}
How it should look like (iPhone 12)
Buggy view(iPhone 12 Pro Max)
The problem is here — setting a fixed frame of 350 isn't going to scale well to other screen sizes:
RoundedRectangle(cornerRadius: 15).fill(Color.white).frame(width: 350, height: 60)
Also, instead of ZStack, use .background — ZStack are usually used for bigger views. Another thing you can do is use .padding instead of Spacer().frame(width: 25) — spacers are designed to expand to fill as much space as possible, so limiting its frame to 25 doesn't really make sense.
struct NewStruct: View {
#State var someState = false
var body: some View {
VStack {
HStack {
Text("Text1")
Spacer()
}
.padding(.horizontal, 25)
Button(action: {
someState = !someState
}, label: {
HStack {
HStack {
Image(systemName: "bolt.fill").resizable().scaledToFit().frame(width: 35, height: 35).cornerRadius(30).padding()
Text("Text1").foregroundColor(Color.black)
Spacer()
Image(systemName: "poweroff").resizable().frame(width: 25, height: 25)
Image(systemName: "power").resizable().frame(width: 25, height: 25)
}
.padding(.horizontal, 10)
}
.background( /// here!
RoundedRectangle(cornerRadius: 15).fill(Color.white)
)
})
}
}
}
struct ContentView: View {
var body: some View {
/// ZStacks are made for bigger views, like a red color that fills the screen.
ZStack {
Color.red
NewStruct()
}
}
}
Result:
Related
I'm trying to make a custom navigation bar with back button, image, VStack (2 labels) but it didn't work. The whole view will stick to the center and not following the alignment I set. Thank you!
struct WeatherNavigation: View {
var body: some View {
HStack {
WeatherNavigation()
}
.
.
.
}
}
//
struct WeatherNavigation: View {
var body: some View {
Button(action: {
//action
}, label: {
HStack {
Image("Back")
.foregroundColor(.black)
Image("Weather")
.resizable()
.frame(width: 40, height: 40)
}
})
.frame(width: 100, height: 50, alignment: .leading)
VStack {
Text(weather.description)
.font(.appFont(size: 18))
.foregroundColor(Color(uiColor: .black))
Text(weather.location)
.font(.appFont(size: 12))
.foregroundColor(Color(uiColor: .blue))
}
.frame(width: .infinity, height: 50, alignment: .leading)
}
}
First of all, you shouldn't set frame of the whole view like that. For you problem, it can divide into: make a HStack to store all the container view and make a space between those two of them. Because using HStack you don't need to add leading.
Code will be like this
struct WeatherNavigation: View {
var body: some View {
// make a HStack to store all the attribute
HStack(alignment: .top) {
Button(action: {
//action
}, label: {
HStack {
Image("Back")
.foregroundColor(.black)
Image("Weather")
.resizable()
.frame(width: 40, height: 40)
}
})
.frame(width: 100, height: 50)
VStack {
Text("Tokyo")
.foregroundColor(Color(uiColor: .black))
Text("Japan")
.foregroundColor(Color(uiColor: .blue))
}
// make a space at the end
Spacer()
}
}
}
And the usage like this
struct ContentView: View {
var body: some View {
VStack {
HStack {
WeatherNavigation()
}
Spacer()
}
}
}
More over: adding Spacer() means that make space between view. That will solved your problem if you want to keep like your way.
I am trying to make a SwiftUI TextEditor with a Divider that adapts its position to stay under the bottom-most line of text inside of a edit-bio section of the app.
Note: I have a frame on my TextEditor so that it doesn't take up the whole-screen
Right now the Divider is static and stays in one place. Is there a built-in way to make the divider stay under the bottom most line of text?
I would think the Spacer would have given me this behavior?
Thank you!
struct EditBio: View {
#ObservedObject var editProfileVM: EditProfileViewModel
var body: some View {
VStack(spacing: 10) {
TextEditor(text: $editProfileVM.bio)
.foregroundColor(.white)
.padding(.top, 70)
.padding([.leading, .trailing], 50)
.frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
Divider().frame(height: 1).background(.white)
Spacer()
}
}
}
It is doing exactly what you told it to do. But a background color on your TextEditor. You will see that it has a height of 200 + a spacing of 10 from the VStack.
I changed your code to make it obvious:
struct EditBio: View {
#State var editProfileVM = ""
var body: some View {
VStack(spacing: 10) {
TextEditor(text: $editProfileVM)
.foregroundColor(.white)
.padding(.top, 70)
.padding([.leading, .trailing], 50)
.frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
.background(Color.gray)
Divider().frame(height: 1).background(.red)
Spacer()
}
}
}
to produce this:
You can see the TextEditor naturally wants to be taller than 200, but that is limiting it. Therefore, the Spacer() is not going to cause the TextEditor to be any smaller.
The other problem that setting a fixed frame causes will be that your text will end up off screen at some point. I am presuming what you really want is a self sizing TextEditor that is no larger than it's contents.
That can be simply done with the following code:
struct EditBio: View {
#State var editProfileVM = ""
var body: some View {
VStack(spacing: 10) {
SelfSizingTextEditor(text: $editProfileVM)
// Frame removed for the image below.
// .frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
.foregroundColor(.white)
// made the .top padding to be .vertical
.padding(.vertical, 70)
.padding([.leading, .trailing], 50)
.background(Color.gray)
Divider().frame(height: 5).background(.red)
Spacer()
}
}
}
struct SelfSizingTextEditor: View {
#Binding var text: String
#State var textEditorSize = CGSize.zero
var body: some View {
ZStack {
Text(text)
.foregroundColor(.clear)
.copySize(to: $textEditorSize)
TextEditor(text: $text)
.frame(height: textEditorSize.height)
}
}
}
extension View {
func readSize(onChange: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
func copySize(to binding: Binding<CGSize>) -> some View {
self.readSize { size in
binding.wrappedValue = size
}
}
}
producing this view:
I'm trying to animate in a view and make it appear as if it's a sort of drawer opening from another view. This is all fine except if the first view is not opaque. It appears that you can see the animating view the moment it begins animating. Is there a way to clip this so it appears that the view is growing from the top of the bottom view?
Even without opacity this is an issue if where you're animating in from isn't a covered (demoed in second gif)
Sample Code:
struct ContentView: View {
#State private var showingSecondView: Bool = false
var body: some View {
VStack(spacing: 0) {
Spacer()
if showingSecondView {
ZStack {
Color.green.opacity(0.25)
Text("Second View")
}
.frame(width: 300, height: 300)
.transition(.move(edge: .bottom))
}
ZStack {
Color.black.opacity(1)
Text("First View")
}
.frame(width: 300, height: 300)
Button("Animate In / Out") {
showingSecondView.toggle()
}
.padding()
}
.animation(.easeInOut, value: showingSecondView)
}
}
It is possible to do by clipping exact container of 'drawer'. Here is a demo of possible approach.
Tested with Xcode 13.2 / iOS 15.2 (Simulator slow animation is ON for better demo)
var body: some View {
VStack(spacing: 0) {
Spacer()
VStack {
if showingSecondView {
ZStack {
Color.green.opacity(0.25)
Text("Second View")
}
.transition(.move(edge: .bottom))
} else {
Color.clear // << replacement for transition visibility
}
}
.frame(width: 300, height: 300)
.animation(.easeInOut, value: showingSecondView) // << animate drawer !!
.clipped() // << clip drawer area
ZStack {
Color.black.opacity(0.2)
Text("First View")
}
.frame(width: 300, height: 300)
Button("Animate In / Out") {
showingSecondView.toggle()
}
.padding()
}
}
Here a way for you:
struct ContentView: View {
#State private var isSecondViewPresented: Bool = false
var body: some View {
VStack(spacing: 0) {
Spacer()
ZStack {
Color.green.opacity(0.25).cornerRadius(20)
Text("Second View")
}
.frame(width: 300, height: 300)
.offset(y: isSecondViewPresented ? 0 : 300)
.clipShape(RoundedRectangle(cornerRadius: 20))
ZStack {
Color.black.opacity(0.1).cornerRadius(20)
Text("First View")
}
.frame(width: 300, height: 150)
Button("Animate In / Out") {
isSecondViewPresented.toggle()
}
.padding()
}
.animation(.easeInOut, value: isSecondViewPresented)
}
}
I am trying to create an Instagram-like UI with SwiftUI, and since I wasn't able to resize the tab elements in the TabView, I decided to write a simple CustomTabView instead. But I end up with a padding at the top of it and I don't understand why. Here is the code:
struct ContentView: View {
#State private var selectedIndex: Int = 0
var body: some View {
VStack {
switch selectedIndex {
case 0:
Color.blue
case 1:
Color.yellow
case 2:
Color.red
case 3:
Color.orange
default:
Color.green
}
CustomTabView(selectedIndex: $selectedIndex)
}
}
}
struct CustomTabView: View {
#Binding var selectedIndex: Int
var body: some View {
VStack {
Divider()
HStack {
Button(action: {
selectedIndex = 0
}, label: {
Image("HomeIcon")
.resizable()
.frame(width: 30, height: 30, alignment: .center)
})
Spacer()
Button(action: {
selectedIndex = 1
}, label: {
Image("PlayIcon")
.resizable()
.frame(width: 30, height: 30, alignment: .center)
})
Spacer()
Button(action: {
selectedIndex = 2
}, label: {
Image("AddIcon")
.resizable()
.frame(width: 30, height: 30, alignment: .center)
})
Spacer()
Button(action: {
selectedIndex = 3
}, label: {
Image("HeartIcon")
.resizable()
.frame(width: 30, height: 30, alignment: .center)
})
Spacer()
Button(action: {
selectedIndex = 4
}, label: {
Image("ProfileIcon")
.resizable()
.frame(width: 30, height: 30, alignment: .center)
.cornerRadius(15)
})
}
.padding(.horizontal, 24)
.padding(.top, 4)
}
.background(Color.white)
}
}
The result I'm getting:
What am I doing wrong?
Thank you for your help
If the problem is the gap between the blue box and the divider, try setting the spacing of the VStack to 0:
VStack(spacing: 0) {
...
}
Top Space
That is the safe area. You can ignore it with this modifier:
.ignoresSafeArea()
Apply it to the view you want it to extend beyond the safe area, for example:
Color.blue
.ignoresSafeArea(.container, edges: .top)
Bottom Space
That is the spacing of the VStack. Get rid of it by setting that to 0:
VStack(spacing: 0) {
,,,
}
try this
Image("background").resizable()
.scaledToFill()
.clipped()
.edgesIgnoringSafeArea([.top])
I am trying to change background color main this view but unable to do it. I tried to put background(Color.green) at HStack, VSTack and even on ZStack but it did not work, not sure if i am putting at right place. By default it is taking phone or simulator color which is white but i want to apply custom background color
My Xcode version is 11.5
struct HomePageView: View {
#State var size = UIScreen.main.bounds.width / 1.6
var body: some View {
GeometryReader{_ in
VStack {
HStack {
ZStack{
// main home page components here....
NavigationView{
VStack {
AssignmentDaysView()
}.background(Color.lairBackgroundGray)
.frame(width: 100, height: 100, alignment: .top)
.navigationBarItems(leading: Button(action: {
self.size = 10
}, label: {
Image("menu")
.resizable()
.frame(width: 30, height: 30)
}).foregroundColor(.appHeadingColor), trailing:
Button(action: {
print("profile is pressed")
}) {
HStack {
NavigationLink(destination: ProfileView()) {
LinearGradient.lairHorizontalDark
.frame(width: 30, height: 30)
.mask(
Image(systemName: "person.crop.circle")
.resizable()
.scaledToFit()
)
}
}
}
).navigationBarTitle("Home", displayMode: .inline)
}
HStack{
menu(size: self.$size)
.cornerRadius(20)
.padding(.leading, -self.size)
.offset(x: -self.size)
Spacer()
}
Spacer()
}.animation(.spring()).background(Color.lairBackgroundGray)
Spacer()
}
}
}
}
}
struct HomePageView_Previews: PreviewProvider {
static var previews: some View {
HomePageView()
}
}
In your NavigationView you have a VStack. Instead you can use a ZStack and add a background below your VStack.
Try the following:
NavigationView {
ZStack {
Color.green // <- or any other Color/Gradient/View you want
.edgesIgnoringSafeArea(.all) // <- optionally, if you want to cover the whole screen
VStack {
Text("assignments")
}
.background(Color.gray)
.frame(width: 100, height: 100, alignment: .top)
}
}
Note: you use many stacks wrapped in a GeometryReader which you don't use. Consider simplifying your View by removing unnecessary stacks. Also you may not need a GeometryReader if you use UIScreen.main.bounds (however, GeometryReader is preferred in SwiftUI).
Try removing some layers: you can start with removing the top ones: GeometryReader, VStack, HStack...
Try the following:
Change the view background color especially safe area also
struct SignUpView: View {
var body: some View {
ZStack {
Color.blue //background color
}.edgesIgnoringSafeArea(.all)