Center Swiftui View in Top-Level View

Center SwiftUI view in top-level view

Here is a demo of possible approach. The idea is to use injected UIView to access UIWindow and then show loading view as a top view of window's root viewcontroller view.

Tested with Xcode 12 / iOS 14 (but SwiftUI 1.0 compatible)

Sample Image

Note: animations, effects, etc. are possible but are out scope for simplicity

struct CenteredLoadingView<RootView: View>: View {
private let rootView: RootView
@Binding var isActive: Bool

init(rootView: RootView, isActive: Binding<Bool>) {
self.rootView = rootView
self._isActive = isActive
}

var body: some View {
rootView
.background(Activator(showLoading: $isActive))
}

struct Activator: UIViewRepresentable {
@Binding var showLoading: Bool
@State private var myWindow: UIWindow? = nil

func makeUIView(context: Context) -> UIView {
let view = UIView()
DispatchQueue.main.async {
self.myWindow = view.window
}
return view
}

func updateUIView(_ uiView: UIView, context: Context) {
guard let holder = myWindow?.rootViewController?.view else { return }

if showLoading && context.coordinator.controller == nil {
context.coordinator.controller = UIHostingController(rootView: loadingView)

let view = context.coordinator.controller!.view
view?.backgroundColor = UIColor.black.withAlphaComponent(0.8)
view?.translatesAutoresizingMaskIntoConstraints = false
holder.addSubview(view!)
holder.isUserInteractionEnabled = false

view?.leadingAnchor.constraint(equalTo: holder.leadingAnchor).isActive = true
view?.trailingAnchor.constraint(equalTo: holder.trailingAnchor).isActive = true
view?.topAnchor.constraint(equalTo: holder.topAnchor).isActive = true
view?.bottomAnchor.constraint(equalTo: holder.bottomAnchor).isActive = true
} else if !showLoading {
context.coordinator.controller?.view.removeFromSuperview()
context.coordinator.controller = nil
holder.isUserInteractionEnabled = true
}
}

func makeCoordinator() -> Coordinator {
Coordinator()
}

class Coordinator {
var controller: UIViewController? = nil
}

private var loadingView: some View {
VStack {
Color.white
.frame(width: 48, height: 72)
Text("Loading")
.foregroundColor(.white)
}
.frame(width: 142, height: 142)
.background(Color.primary.opacity(0.7))
.cornerRadius(10)
}
}
}

struct CenterView: View {
@State private var isLoading = false
var body: some View {
return VStack {
Color.gray
HStack {
CenteredLoadingView(rootView: list, isActive: $isLoading)
otherList
}
Button("Demo", action: load)
}
.onAppear(perform: load)
}

func load() {
self.isLoading = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.isLoading = false
}
}

var list: some View {
List {
ForEach(1..<6) {
Text($0.description)
}
}
}

var otherList: some View {
List {
ForEach(6..<11) {
Text($0.description)
}
}
}
}

How To Position Views Relative To Their Top Left Corner In SwiftUI

@Asperi 's answer will solve the problem. But, I think we should use Spacer() rather than Color.clear and ZStack.

Spacer is specifically designed for these scenarios and makes the code easier to understand.

struct HomeView: View {
var body: some View {
HStack {
VStack(alignment: .leading) {
Text("Top Text")
.font(.system(size: 20))
.fontWeight(.medium)

Text("Bottom Text")
.font(.system(size: 12))
.fontWeight(.regular)
Spacer()
}
Spacer()
}
}
}

SwiftUI layout system is different from UIKit.

It asks each child view to calculate its own size based on the bounds of its parent view. Next, asks each parent to position its children within its own bounds.

https://www.hackingwithswift.com/books/ios-swiftui/how-layout-works-in-swiftui

SwiftUI set position to center of different view

Here is possible approach (with a bit simplified your initial snapshot and added some convenient View extension).

Tested with Xcode 11.2 / iOS 13.2

Sample Image

extension View {
func rectReader(_ binding: Binding<CGRect>, in space: CoordinateSpace) -> some View {
self.background(GeometryReader { (geometry) -> AnyView in
let rect = geometry.frame(in: space)
DispatchQueue.main.async {
binding.wrappedValue = rect
}
return AnyView(Rectangle().fill(Color.clear))
})
}
}

struct ContentView: View {

@State private var tap = false
@State private var bottomRect: CGRect = .zero

var body: some View {
ZStack(alignment: .bottom) {
RoundedRectangle(cornerRadius: 10)
.frame(maxWidth: .infinity, maxHeight: 50, alignment: .center)
.padding()
.rectReader($bottomRect, in: .named("board"))

Rectangle()
.foregroundColor(.red)
.overlay(Text("Click me")
.fontWeight(.light)
.foregroundColor(.white)
)
.frame(width: 50, height: 50)
.position(x: self.tap ? bottomRect.midX : 50,
y: self.tap ? bottomRect.midY : 50)
.onTapGesture {
withAnimation {
self.tap.toggle()
}
}
}.coordinateSpace(name: "board")
}
}

SwiftUI: Align a View to the center of its Parent

Here is one case with Zstack:

  struct  ListMissionView : View{

var missions: [Mission] = [
Mission(name: "Cassini",
launchDate: "10/15/97",
launchLocation: "Cape Canaveral",
missionEndDate: "09/15/17",
status: "Past"),
Mission(name: "Galaxy Evolution Explorer",
launchDate: "04/28/03",
launchLocation: "Cape Canaveral",
missionEndDate: "06/28/13",
status: "Past"),
Mission(name: "IRAS",
launchDate: "01/25/83",
launchLocation: "Vandenberg",
missionEndDate: "11/21/83",
status: nil),
Mission(name: "NuSTAR",
launchDate: "06/13/12",
launchLocation: "Central Pacific Ocean",
missionEndDate: nil,
status: "Current"),
Mission(name: "Voyager 1",
launchDate: "09/05/77",
launchLocation: "Central Pacific Ocean",
missionEndDate: nil,
status: nil)
]

var body: some View {
GeometryReader{ proxy in
List {
ForEach(self.missions, id: \.name) { mission in
MissionCell(mission: mission, proxy: proxy)
}
}
}
}
}

struct MissionCell: View {
var mission: Mission

var proxy: GeometryProxy
var body: some View {

// VStack(alignment: .missionOptionalInfoAlignment, spacing: 2) {
VStack(alignment: .leading, spacing: 2) {
Text(self.mission.name)
.lineLimit(1)
.truncationMode(.middle)

ZStack(alignment: .topLeading) {

VStack(alignment: .leading) {
HStack {
Image(systemName: "calendar")
.foregroundColor(Color(UIColor.systemGray))
Text(self.mission.launchDate)
.foregroundColor(Color(UIColor.systemGray))
}
HStack {
Image(systemName: "location.fill")
.foregroundColor(Color(UIColor.systemGray))
Text(self.mission.launchLocation)
.foregroundColor(Color(UIColor.systemGray))
}

}

VStack(alignment: .leading) {

if self.mission.missionEndDate != nil {
HStack {
Image(systemName: "hand.raised.slash.fill")
.foregroundColor(Color(UIColor.systemGray))
Text(self.mission.missionEndDate!)
.foregroundColor(Color(UIColor.systemGray))
}
}
if self.mission.status != nil {
HStack {
Image(systemName: "checkmark")
.foregroundColor(Color(UIColor.systemGray))
Text(self.mission.status ?? "")
.foregroundColor(Color(UIColor.systemGray))

}
}
}.alignmentGuide(.leading){ _ in return -self.proxy.size.width / 2.0}
}
}
.padding([.leading, .trailing], 16)
}

}

Top alignment/position of text in SwiftUI

You can use a Spacer() after the Text View to push it upwards.

struct ContentView: View {
var body: some View {
VStack {
Text("Hello World!")
Spacer()
}
}
}

SwiftUI view is in the middle instead of in the top

I had the same issue, but found the the problem I had was in using navigationView multiple times.

I thought that we should have NavigationView in every view, but apparently, we should place navigationView only in the main view of the application, and all other views that we enter via navigationLink, automatically get the back option without the need to mention NavigationView again.

So check if you use navigationView more than once in your code.

Strangely, we can specify navigationBarTitle even in views that dont have the navigationView mentioned in them, this is because the navigationView is at the parent view.
Use "displayMode: .inline", to make the navigation area as minimal as possible, it will save you real-estate.

Another thing to do, is to use Spacer().
If you want all your items to be at the top, just put Spacer() as the last item in VStack, and it will push all items to the top.

See this example:

VStack {
// What you care about displaying
Text("Something to Show 1")
Text("Something to Show 2")
Text("Something to Show 3")

// This should be the last, put everything to the top
Spacer()
}
.navigationBarTitle(Text("The Title"), displayMode: .inline)


Related Topics



Leave a reply



Submit