For a SwiftUI layout in a macOS app, I have three Rectangles as shown below:
The code to produce this layout is:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
HStack {
ZStack {
Rectangle()
.fill(Color.purple)
.frame(width: 20)
Text("1")
.font(.subheadline)
.foregroundColor(.white)
}
ZStack {
Rectangle()
.fill(Color.orange)
Text("2")
.font(.subheadline)
.foregroundColor(.white)
}
}
HStack {
ZStack {
Rectangle()
.fill(Color.red)
.frame(height: 20)
Text("3")
.font(.subheadline)
.foregroundColor(.white)
}
}
}
.frame(minWidth: 400, minHeight: 250)
}
}
My objective is for Rectangle 1 to be the same height as Rectangle 2 and for Rectangle 3 to be the same width as Rectangle 2. The size relationships between the rectangles should stay the same as the window size is changed. When done correctly, the final result should look like the following:
How can I accomplish this in SwiftUI?
Here is a working approach, based on view preferences. Tested with Xcode 11.4 / macOS 10.15.6
struct ViewWidthKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}
struct ContentView: View {
#State private var boxWidth = CGFloat.zero
var body: some View {
VStack {
HStack {
ZStack {
Rectangle()
.fill(Color.purple)
.frame(width: 20)
Text("1")
.font(.subheadline)
.foregroundColor(.white)
}
ZStack {
Rectangle()
.fill(Color.orange)
Text("2")
.font(.subheadline)
.foregroundColor(.white)
}
.background(GeometryReader {
Color.clear.preference(key: ViewWidthKey.self,
value: $0.frame(in: .local).size.width) })
}
HStack {
ZStack {
Rectangle()
.fill(Color.red)
.frame(height: 20)
Text("3")
.font(.subheadline)
.foregroundColor(.white)
}.frame(width: boxWidth)
}.frame(maxWidth: .infinity, alignment: .bottomTrailing)
}
.onPreferenceChange(ViewWidthKey.self) { self.boxWidth = $0 }
.frame(minWidth: 400, minHeight: 250)
}
}
Related
I've made a custom tab bar and I'm trying to add a search bar on one of my views. For some reason half of the tab gets pushed up when the onscreen keyboard appears. Ive tried " .ignoresSafeArea(.keyboard)" literally everywhere and the only thing that happens is my icons for the tab disappear but the top of the tab still stays there. I've been trying to fix it for the last 24 hours but I'm getting nowhere, can somebody please help me with this. Thanks!
Code for Custom Tab Bar:
import SwiftUI
enum Tabs: Int {
case home = 0
case hot = 1
case favourites = 2
case settings = 3
}
struct TabBar: View {
#Binding var selectedTab : Tabs
var body: some View {
HStack (alignment: .center){
Button {
//switch to home
selectedTab = .home
} label: {
GeometryReader { geo in
if selectedTab == .home {
Rectangle()
// .ignoresSafeArea(.keyboard)
.foregroundColor(.red)
.frame(width: geo.size.width/2, height: 5)
.padding(.leading, geo.size.width/4)
}
VStack (alignment: .center){
Image(systemName: "house")
// .ignoresSafeArea(.keyboard)
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
.padding(.top, 40.0)
}
// .ignoresSafeArea(.keyboard)
.frame(width: geo.size.width, height: geo.size.height)
}
//.ignoresSafeArea(.keyboard)
.offset(y: -35)
//.ignoresSafeArea(.keyboard)
}
// .ignoresSafeArea(.keyboard)
.tint(Color.black)
Button {
//Switch views
selectedTab = .hot
} label: {
GeometryReader { geo in
if selectedTab == .hot {
Rectangle()
//.ignoresSafeArea(.keyboard)
.foregroundColor(.red)
.frame(width: geo.size.width/2, height: 5)
.padding(.leading, geo.size.width/4)
}
VStack (alignment: .center){
Image(systemName: "flame")
//.ignoresSafeArea(.keyboard)
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
.padding(.top, 40.0)
}
//.ignoresSafeArea(.keyboard)
.frame(width: geo.size.width, height: geo.size.height)
}
//.ignoresSafeArea(.keyboard)
.offset(y: -35)
}
//.ignoresSafeArea(.keyboard)
.tint(Color.black)
Button {
//Switch views
selectedTab = .favourites
} label: {
GeometryReader { geo in
if selectedTab == .favourites {
Rectangle()
//.ignoresSafeArea(.keyboard)
.foregroundColor(.red)
.frame(width: geo.size.width/2, height: 5)
.padding(.leading, geo.size.width/4)
}
VStack (alignment: .center){
Image(systemName: "star")
//.ignoresSafeArea(.keyboard)
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
.padding(.top, 40.0)
}//.ignoresSafeArea(.keyboard)
.frame(width: geo.size.width, height: geo.size.height)
}
//.ignoresSafeArea(.keyboard)
.offset(y: -35)
}
//.ignoresSafeArea(.keyboard)
.tint(Color.black)
Button {
//Switch views
selectedTab = .settings
} label: {
GeometryReader { geo in
if selectedTab == .settings {
Rectangle()
//.ignoresSafeArea(.keyboard)
.foregroundColor(.red)
.frame(width: geo.size.width/2, height: 5)
.padding(.leading, geo.size.width/4)
}
VStack (alignment: .center){
Image(systemName: "gear")
//.ignoresSafeArea(.keyboard)
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
.padding(.top, 40.0)
}//.ignoresSafeArea(.keyboard)
.frame(width: geo.size.width, height: geo.size.height)
}
//.ignoresSafeArea(.keyboard)
.offset(y: -35)
}
//.ignoresSafeArea(.keyboard)
.tint(Color.black)
}
//.ignoresSafeArea(.keyboard, edges: .all)
.frame(height: 20)
}
}
struct TabBar_Previews: PreviewProvider {
static var previews: some View {
TabBar(selectedTab: .constant(.home))
.ignoresSafeArea(.keyboard)
}
}
ignoresSafeArea is commented in all the places I tried to put it.
Here is also the code for the search bar, maybe I need to input ignoreSafeArea somewhere here?
import SwiftUI
struct SearchBarView: View {
#StateObject var im = SearchBarContents()
//#State var selectedTabs: SearchB = .search
#State private var query = ""
var body: some View {
NavigationView {
List {
ForEach(im.filteredData) { item in
HStack{
NavigationLink(destination: ItemView(item: item))
{
SBarView(item: item)
}
}
}
}
.navigationTitle("Items")
.searchable(text: $query,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Find an Item") {
}
.onSubmit(of: .search) {
im.search(with: query)
}
.onChange(of: query) { newQuery in
im.search(with: newQuery)
}
.onAppear {
im.search()
}
}
}
}
struct SearchBarView_previews: PreviewProvider {
static var previews: some View {
SearchBarView()
}
}
If there is anything missing please let me know.
Imgur link to see exactly what I mean
https://imgur.com/fv79bKh
https://imgur.com/a/Rx9Ki6c (after I add ignoresafearea)
Depends on how you have set up your project. If you have a RootView which is controlling the View to be displayed based on the selection of your Tab Bar and is also the location in the project that you would add the Custom Tab Bar, then on that root View add the modifier .ignoresSafeArea(.keyboard)
For example:
enum Tabs: Int {
case home = 0
case hot = 1
case favourites = 2
case settings = 3
}
struct RootView: View {
#State private var selectedTab = Tabs.home
var body: some View {
VStack {
switch selectedTab {
case .home:
HomeView()
case .hot:
HotView()
case .favourites:
FavouritesView()
case .settings:
SettingsView()
}
Spacer()
CustomTabBar(selectedTab: $selectedTab)
}
.ignoresSafeArea(.keyboard)
}
}
I need to add two different types of view in same grid view, there is an alignment issue between these view as shown in give image below. The view which contains image is taking more space from top and bottom even after fixing the size
struct TestView: View {
#StateObject var vm = BusinessProfileViewModel()
#Environment(\.presentationMode) private var presentationMode
let columns = [
GridItem(.flexible(minimum: 100), spacing: 20),
GridItem(.flexible(minimum: 100), spacing: 20)
]
var body: some View {
loadView()
}
}
extension TestView {
func loadView() -> some View {
GeometryReader { geometry in
ZStack(alignment: .bottomTrailing) {
ScrollView(.vertical, showsIndicators: false) {
topBusinessImage(geometry: geometry)
VStack {
featureProduct(geometry: geometry)
}.padding()
}.edgesIgnoringSafeArea(.top)
}
}
}
func topBusinessImage(geometry: GeometryProxy) -> some View {
VStack(spacing: 0) {
// Profile Image Stack
// Banner Zstack
ZStack (alignment: .top) {
Image(ImageName.productPlaceholder.rawValue)
.resizable()
.frame(height: geometry.size.height / 2.5)
}
}
}
func featureProduct(geometry: GeometryProxy) -> some View {
VStack(alignment: .leading) {
// product and services button
HStack {
Text("Products & Services")
.font(.custom(Popins.bold.rawValue, size: 20))
Spacer()
Button {
print("list")
} label: {
Image(ImageName.list.rawValue)
.resizable()
.frame(width: 24, height: 24)
}.padding(5)
}
LazyVGrid(columns: columns, spacing: 30) {
BusinessProductCell()
ProductOnlyDetailCell()
ProductOnlyDetailCell()
BusinessProductCell()
}
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
view that contains image
import SwiftUI
struct BusinessProductCell: View {
var body: some View {
loadView()
}
}
extension BusinessProductCell {
func loadView() -> some View {
VStack(alignment: .leading) {
ZStack(alignment: .topLeading) {
Image(ImageName.business.rawValue)
.resizable()
.frame(height: 205)
.cornerRadius(10)
HStack {
Button {
print("fav")
} label: {
Image(systemName: "heart.fill")
.resizable()
.scaledToFit()
.foregroundColor(UnifiedColor.blue)
.frame(width: 20, height: 20)
}.padding()
Spacer()
Button {
print("fav")
} label: {
Image(systemName: "ellipsis")
.resizable()
.scaledToFit()
.frame(width: 20)
.foregroundColor(.white)
}.padding()
}
}
Text("Dove Body Care")
.foregroundColor(.black)
.font(.custom(Popins.medium.rawValue, size: 16))
HStack {
Text("$49.99")
.font(.custom(Popins.medium.rawValue, size: 12))
.foregroundColor(UnifiedColor.textBlue)
Spacer()
HStack {
Text("(286 Favorites)")
.font(.custom(Popins.regular.rawValue, size: 8))
.foregroundColor(.gray)
Image(ImageName.heart_line.rawValue)
.resizable()
.frame(width: 15, height: 15)
}
}
}
}
}
struct BusinessProductCell_Previews: PreviewProvider {
static var previews: some View {
BusinessProductCell()
.frame(width: 200, height: 250)
}
}
View that only contains text or second view for gird view
struct ProductOnlyDetailCell: View {
var body: some View {
loadView()
}
}
extension ProductOnlyDetailCell {
func loadView() -> some View {
VStack(alignment: .leading, spacing: 10) {
HStack {
Image(ImageName.productPlaceholder.rawValue)
.resizable()
.scaledToFill()
.frame(width: 24, height: 24)
.clipShape(Circle())
.overlay {
Circle().stroke(.white, lineWidth: 1)
}
Text("Anton Jr.")
.font(.custom(Popins.regular.rawValue, size: 12 ))
.foregroundColor(.black)
.lineLimit(1)
Spacer()
Button {
print("more btn")
} label: {
Image(systemName: "ellipsis")
.resizable()
.foregroundColor(.black)
.frame(width: 18, height: 4)
.padding([.trailing, .top, .bottom])
}
}
Text("DJ for night")
.font(.custom(Popins.bold.rawValue, size: 14))
.foregroundColor(.black)
Text("Lorem ipsum dolor sitamet, consectetur adipiscingelit. Lectus idcommodoegestas metusinterdum dolor.")
.multilineTextAlignment(.leading)
.font(.custom(Popins.regular.rawValue, size: 12))
.foregroundColor(.black)
.opacity(0.8)
Spacer()
HStack (spacing: 0){
Text("$15K")
.font(.custom(Popins.bold.rawValue, size: 14))
.foregroundColor(.black)
Text("/")
.font(.custom(Popins.bold.rawValue, size: 14))
.foregroundColor(.gray)
Text("Night")
.font(.custom(Popins.regular.rawValue, size: 14))
.foregroundColor(.gray)
}
Text("25 minute ago")
.font(.custom(Popins.regular.rawValue, size: 10))
.foregroundColor(.gray)
}
.padding(10)
.background(
RoundedRectangle(cornerRadius: 10)
.foregroundColor(.gray.opacity(0.1))
)
}
}
I'd like to implement an animation in SwiftUI that "reveals" the content of a view to enable expand/collapse functionality. The content of the view I want to collapse and expand is complex: It's not just a simple box, but it's a view hierarchy of dynamic height and content, including images and text.
I've experimented with different options, but it hasn't resulted in the desired effect. Usually what happens is that when I "expand", the whole view was shown right away with 0% opacity, then gradually faded in, with the buttons under the expanded view moving down at the same time. That's what happened when I was using a conditional if statement that actually added and removed the view. So that makes sense.
I then experimented with using a frame modifier: .frame(maxHeight: isExpanded ? .infinity : 0). But that resulted in the contents of the view being "squished" instead of revealed.
I made a paper prototype of what I want:
Any ideas on how to achieve this?
Something like this might work. You can modify the height of what you want to disclose to be 0 when hidden or nil when not so that it'll go for the height defined by the views. Make sure to clip the view afterwards so the contents are not visible outside of the frame's height when not disclosed.
struct ContentView: View {
#State private var isDisclosed = false
var body: some View {
VStack {
Button("Expand") {
withAnimation {
isDisclosed.toggle()
}
}
.buttonStyle(.plain)
VStack {
GroupBox {
Text("Hi")
}
GroupBox {
Text("More details here")
}
}
.frame(height: isDisclosed ? nil : 0, alignment: .top)
.clipped()
HStack {
Text("Cancel")
Spacer()
Text("Book")
}
}
.frame(maxWidth: .infinity)
.background(.thinMaterial)
.padding()
}
}
No, this wasn't trying to match your design, either. This was just to provide a sample way of creating the animation.
Consider the utilization of DisclosureGroup. The following code should be a good approach to your idea.
struct ContentView: View {
var body: some View {
List(0...20, id: \.self) { idx in
DisclosureGroup {
HStack {
Image(systemName: "person.circle.fill")
VStack(alignment: .leading) {
Text("ABC")
Text("Test Test")
}
}
HStack {
Image(systemName: "globe")
VStack(alignment: .leading) {
Text("ABC")
Text("X Y Z")
}
}
HStack {
Image(systemName: "water.waves")
VStack(alignment: .leading) {
Text("Bla Bla")
Text("123")
}
}
HStack{
Button("Cancel", role: .destructive) {}
Spacer()
Button("Book") {}
}
} label: {
HStack {
Spacer()
Text("Expand")
}
}
}
}
The result looks like:
I coded this in under 5 minutes. So of course the design can be optimized to your demands, but the core should be understandable.
import SwiftUI
struct TaskViewCollapsible: View {
#State private var isDisclosed = false
let header: String = "Review Page"
let url: String
let tasks: [String]
var body: some View {
VStack {
HStack {
VStack(spacing: 5) {
Text(header)
.font(.system(size: 22, weight: .semibold))
.foregroundColor(.black)
.padding(.top, 10)
.padding(.horizontal, 20)
.frame(maxWidth: .infinity, alignment: .leading)
Text(url)
.font(.system(size: 12, weight: .regular))
.foregroundColor(.black.opacity(0.4))
.padding(.horizontal, 20)
.frame(maxWidth: .infinity, alignment: .leading)
}
Spacer()
Image(systemName: self.isDisclosed ? "chevron.up" : "chevron.down")
.padding(.trailing)
.padding(.top, 10)
}
.onTapGesture {
withAnimation {
isDisclosed.toggle()
}
}
FetchTasks()
.padding(.horizontal, 20)
.padding(.bottom, 5)
.frame(height: isDisclosed ? nil : 0, alignment: .top)
.clipped()
}
.background(
RoundedRectangle(cornerRadius: 8)
.fill(.black.opacity(0.2))
)
.frame(maxWidth: .infinity)
.padding()
}
#ViewBuilder
func FetchTasks() -> some View {
ScrollView(.vertical, showsIndicators: true) {
VStack {
ForEach(0 ..< tasks.count, id: \.self) { value in
Text(tasks[value])
.font(.system(size: 16, weight: .regular))
.foregroundColor(.black)
.padding(.vertical, 0)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
.frame(maxHeight: CGFloat(tasks.count) * 20)
}
}
struct TaskViewCollapsible_Previews: PreviewProvider {
static var previews: some View {
TaskViewCollapsible(url: "trello.com", tasks: ["Hello", "Hello", "Hello"])
}
}
I'm trying to make the circles fit into the HStack such that the HStack size does not increase.
How can I make the circles fit without specifying a fixed frame?
struct ContentView: View {
var body: some View {
NavigationView {
Form {
HStack {
Circle()
.fill(Color.red)
.aspectRatio(1, contentMode: .fit)
Text("Hello")
}
HStack {
Circle()
.fill(Color.blue)
.aspectRatio(1, contentMode: .fit)
Text("Hello")
}
}
}
}
}
Here is a sample of various containers to chose from. SwiftUI will do all the layout, automatically handle rotations and device resolutions.
struct CirclesView: View {
var body: some View {
VStack(spacing: 0) {
Label("Circles", systemImage: "circle").font(.system(size: 24, weight: .black, design: .rounded)).foregroundColor(.pink)
HStack {
Circle()
.foregroundColor(.yellow)
.frame(width: 32, height: 32)
Text("This is a yellow circle")
Spacer()
}
Circle()
.foregroundColor(.orange)
.shadow(radius: 10)
.frame(width: 75)
Divider()
HStack {
VStack {
Circle().foregroundColor(.blue)
Text("Blue").font(.title3)
HStack {
Circle().foregroundColor(.purple)
Text("Purple").font(.caption)
}
}
.padding()
.background(Color.yellow)
ZStack(alignment: Alignment(horizontal: .center, vertical: .center)) {
Circle().foregroundColor(.green)
Text("Green").foregroundColor(.primary)
}
}
}
}
}
I want the Text("111") to have the equal height of the VStack containing 2222... and 333....
struct Test7: View {
var body: some View
{ HStack (alignment: .top) {
Text( "111") // Shall have equal Height
.background(Color.red)
VStack(alignment: .leading){. // of this VStack
Text("2222222")
.background(Color.gray)
Text("333333333")
.background(Color.blue)
}
}
.background(Color.yellow)}
}
I tried with a GeometryReaderbut didn't get it to work
Here is possible approach using .alignmentGuide
struct Test7: View {
#State
private var height: CGFloat = .zero // < calculable height
var body: some View {
HStack (alignment: .top) {
Text( "111")
.frame(minHeight: height) // in Preview default is visible
.background(Color.red)
VStack(alignment: .leading) {
Text("2222222")
.background(Color.gray)
Text("333333333")
.background(Color.blue)
}
.alignmentGuide(.top, computeValue: { d in
DispatchQueue.main.async { // << dynamically detected - needs to be async !!
self.height = max(d.height, self.height)
}
return d[.top]
})
}
.background(Color.yellow)
}
}
Note: real result is visible only in LivePreview, because height is calculated dynamically and assign in next rendering cycle to avoid conflicts on #State.
use .frame(maxHeight: .infinity)
var body: some View {
HStack(alignment: .top) {
Text("111")
.frame(maxHeight: .infinity)
.background(Color.red)
VStack(alignment: .leading) {
Text("2222222")
.frame(maxHeight: .infinity)
.background(Color.gray)
Text("333333333")
.frame(maxHeight: .infinity)
.background(Color.blue)
}
}.background(Color.yellow)
.frame(height: 50)
}
result: demo