Make SwiftUI View embedded in Storyboard ViewController use full Screen Width - ios

Problem
Hello, I need help with a SwiftUI View that is presented by a View Controller that is configured in a storyboard.
I present a SwiftUI View including its own model using a UIHostingController as described in this post: addSubView SwiftUI View to UIKit UIView in Swift.
Now my view is shrunken inside its Hosting View Controller's view property to its content size.
What I have tried
Wrapping a ContainerView inside the Parent View Controller into two StackViews to force the view to use the whole screen (vertical and horizontal)
Configuring the initial VStack inside my SwiftUI View with .frame(minWidth: 0, maxWidth: .infinity)
Using an initial HStack with a Spacer() beneath and below all elements within that stack as suggested in Make a VStack fill the width of the screen in SwiftUI
Some Code
View Controller:
class SomeParentViewController: UIViewController {
var detailsView: DetailsOverlayView? // --> SwiftUI View
var detailsViewModel: DetailsOverlayViewModel? // --> Its model
var childVC: UIHostingController<DetailsOverlayView>? // --> Child VC to present View
// inside UIView
var detailData: DetailData?
override func viewDidLoad() {
super.viewDidLoad()
if let data = detailData {
model = DetailsOverlayViewModel(data: data)
}
if let m = model {
detailsView = DetailsOverlayView(viewModel: m)
}
if let v = paymentDetailsView {
child = UIHostingController(rootView: v)
child?.view.translatesAutoresizingMaskIntoConstraints = false
child?.view.frame = self.view.bounds
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let cvc = childVC {
self.addSubview(cvc.view)
self.addChild(cvc)
}
}
...
}
The SwiftUI View:
...
var body: some View {
VStack(alignment: .leading, spacing: 16) {
// Title
HStack {
Text("My great and awesome title")
}
VStack(alignment: .leading, spacing: 0) {
Text("My even more awesome subtitle")
HStack(alignment: .top) {
Text("on the left 1")
Spacer()
Text("on the right 1")
}
HStack(alignment: .top) {
Text("on the left 2")
Spacer()
Text("on the right 2")
}
Divider()
}
}
.frame(minWidth: 0, maxWidth: .infinity)
.padding(16)
}
...
A Picture of the Result

Well considering a SwiftUI view wrapped in a UIHostingController acts the same way a UIViewController does, you would define autolayout constraints the same way.
Having declared translatesAutoresizingMaskIntoConstraints = false, I'm going to assume that's the plan. In that case, what I'd suggest is the following:
NSLayoutConstraint.activate([
child.view.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
child.view.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
child.view.widthAnchor.constraint(equalTo: self.view.widthAnchor),
child.view.heightAnchor.constraint(equalTo: self.view.heighAnchor)
])
The above makes the UIHostingController the same size as the parent controller and centered to it. The height and width anchors can be constants such that you'd do .constraint(equalToConstant: 150), or whatever other variation you'd prefer.

Related

SwiftUI - Adding dimmer view behind custom alert

I created a view modifier for reusable custom alert which works as expected. I now want to add a dimmer view in between presenting view and alert view i.e. view behind the alert which covers full screen and disables any clicks on the presenting view.
I tried adding background on the presenting view when alert is presented, but nothing is happening.
Custom alert view modifier, view extension and view model:
import Foundation
import SwiftUI
struct CustomAlertView: ViewModifier {
#Binding var isPresented: Bool
init(isPresented: Binding<Bool>) {
self._isPresented = isPresented
}
func body(content: Content) -> some View {
content.overlay(alertContent())
}
#ViewBuilder
private func alertContent() -> some View {
GeometryReader { geometry in
if self.$isPresented.wrappedValue {
VStack {
Image(systemName: "info.circle").resizable().frame(width: 30.0, height: 30.0).padding(.top, 30).foregroundColor(.cyan)
Text("Error title").foregroundColor(Color.black).font(.title2).bold().lineLimit(nil).padding([.leading, .trailing], 24.0).padding(.top, 16.0)
Spacer()
Text("There was an error while processing your request.").foregroundColor(Color.black).font(.body).lineLimit(nil).padding([.leading, .trailing], 18.0).padding(.top, 16.0)
Spacer()
Button(action: { self.$isPresented.wrappedValue.toggle() }) {
Text("Ok").foregroundColor(.white).font(.largeTitle).bold()
}.padding(.bottom, 25.0)
}.fixedSize(horizontal: false, vertical: true)
.background(Color.purple)
.cornerRadius(10)
.clipped()
.padding([.leading, .trailing], 5.0)
.position(x: geometry.size.width/2, y: geometry.size.height/2)
.frame(width: 328.0)
}
}
}
}
extension View {
func customAlert(isPresented: Binding<Bool>) -> some View {
return modifier(CustomAlertView(isPresented: isPresented))
}
}
class CustomViewModel: ObservableObject {
#Published var showAlert = false
func doSomething() {
// Sets showAlert to true incase of network disconnect or some query failure.
self.showAlert = true
}
}
Content view:
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel: CustomViewModel = CustomViewModel()
var body: some View {
VStack {
Spacer()
Button(action: { viewModel.doSomething() }) {
Text("Start").foregroundColor(Color.red).font(.title)
}.padding(.bottom, 100.0)
}
.background(viewModel.showAlert ? Color.gray : Color.clear)
.customAlert(isPresented: $viewModel.showAlert)
}
}
Here, ContentView is the presenting view since it's what is presenting the alert. I want to add a grayish sort of view/dimmer view covering full screen and it will be below the presented alert. When dimmer view is present and I click on "Start" button in ContentView, it should be disabled. I don't know if I can achieve this by modifying the custom alert view modifier, hence I was trying to add a background color to ContentView, but nothing seems to be happening. I have too much of code in the view model and content view, so I removed most of it and kept what I thought was needed.
How do I achieve it?
I was able to add a view behind the custom alert view modifier. Adding code here incase if someone comes looking for it in future.
I removed ".background(viewModel.showAlert ? Color.gray : Color.clear)" from my content view since I eventually wanted the logic to be part of custom alert view modifier and not add it to every view. In my customAlertView, I modified the body function as below:
func body(content: Content) -> some View {
content.overlay(self.$isPresented.wrappedValue ? Color.gray.ignoresSafeArea() : nil)
.overlay(self.$isPresented.wrappedValue ? alertContent() : nil)
}

The z-index of SwiftUI View and UIKit View

The SwiftUI scroll view is hiding some area of a view presented from UIViewControllerRepresentable viewController.
Part of the SwiftUI code, The GoogleMapsView is the UIViewControllerRepresentable viewController.
struct ContentView: View {
var body: some View {
ZStack(alignment: .top) {
GoogleMapsView()
VStack(alignment: .leading) {
Text("Top Locations near you")
ScrollView(.horizontal, showsIndicators: false) {
HStack() {
ForEach(dataSource.topPlaces) { place in
PlaceCardView(placeImage: place.image, placeName: place.name, placeRating: place.rating, placeRatingTotal: place.ratingTotal, topPlace: place)
}
}
.padding(.horizontal, 10)
}
.background(Color.clear)
.foregroundColor(.black)
.frame(height: 200)
.opacity(self.finishedFetching ? 1 : 0.1)
.animation(.easeInOut(duration: 1.3))
}.edgesIgnoringSafeArea(.all)
}
The view I want to put on the top is from the GoogleMapView() which I put it on the "bottom" of the ZStack because I want that Scroll view to flow on the map. However the view show on the map when marker is tapped is cover up by the SwiftUI ScrollView
I tried to change their zIndex of the scrollView, zPosition of the pop up view or bringSubviewToFront etc. None of them work.
You need to inject local state via binding into `` and change it there on popup shown/hidden.
Here is some pseudo code
struct ContentView: View {
#State private var isPopup = false
var body: some View {
ZStack(alignment: .top) {
GoogleMapsView(isPopup: $isPopup)
VStack(alignment: .leading) {
Text("Top Locations near you")
if !isPopup {
ScrollView(.horizontal, showsIndicators: false) {
// other code
}
// other code
}
}.edgesIgnoringSafeArea(.all)
}
struct GoogleMapsView : UIViewControllerRepresentable {
#Binding isPopup: Bool
// other code
// make self.isPopup = true before popup and false after

SwiftUI- can't drag a view outside of a ScrollView

Here is a very simplified version of my layout. I have a text view that I'm able to drag around...
struct DragView: View {
var text:String
#State var dragAmt = CGSize.zero
var body: some View {
Text(text)
.padding(15)
.background(Color.orange)
.cornerRadius(20)
.padding(5)
.frame(width: 150, height: 60, alignment: .leading)
.offset(dragAmt)
.gesture(
DragGesture(coordinateSpace: .global)
.onChanged {
self.dragAmt = CGSize(width: $0.translation.width, height: $0.translation.height)
}
.onEnded {_ in
self.dragAmt = CGSize.zero
})
}
}
And then I arrange these views like so:
struct ContentView: View {
var body: some View {
HStack {
ScrollView {
VStack {
DragView(text: "hi")
DragView(text: "hi")
DragView(text: "hi")
}
}
Divider()
VStack {
DragView(text: "hi")
DragView(text: "hi")
DragView(text: "hi")
}
}
}
}
The views that are not in the ScrollView act as expected, and I can drag them around the entire screen. The views that are in the ScrollView, however, will disappear if brought outside the bounds of the ScrollView.
Why does this occur? Is there any way to enable these views to be dragged outside the ScrollView?
Why does this occur?
You drag, you just don't see it because ScrollView clips its content view. Stacks do not, by default. If you add .clipped() for bottom VStack you'll see the same.
Is there any way to enable these views to be dragged outside the
ScrollView?
Solution might be using ZStack. Say on drag you hide one view in ScrollView and show same out-of ScrollView at the same global location, so visually it would behave as would drag original.

SwiftUI | How to override life cycle methods like viewWillAppear

In my existing app, there are few UI configurations like setting up a custom navigation bar, setting a full-screen background images and some more, on all view controllers. For this purpose, configurations are made in a base view controller in viewWillAppear, all view controllers and inherited from it. So configurations are made without doing anything in child view controllers
How to achieve a similar implementation using SwiftUI?
SwiftUI provides onAppear() & onDisappear() methods on View, which is Struct and doesn't support inheritance. If I make extensions methods or protocol, I have to call methods from all Views.
Any help is appreciated...
Make your own container view. You can use #ViewBuilder to make your view behave like VStack and HStack where it takes a content closure full of subviews.
For example, here's a custom container that gives the main content a pink background, and puts a toolbar under it:
import SwiftUI
struct CustomContainer<Content: View>: View {
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
let content: Content
var body: some View {
VStack(spacing: 0) {
content
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(hue: 0, saturation: 0.3, brightness: 1))
Toolbar()
}
}
}
struct Toolbar: View {
var body: some View {
HStack {
Spacer()
button("person.3.fill")
Spacer()
button("printer.fill")
Spacer()
button("heart.fill")
Spacer()
button("bubble.left.fill")
Spacer()
} //
.frame(height: 44)
}
private func button(_ name: String) -> some View {
Image(systemName: name)
.resizable()
.aspectRatio(contentMode: .fit)
.padding(4)
.frame(width: 40, height: 40)
}
}
And then here's a view that uses it:
struct HelloView: View {
var body: some View {
CustomContainer {
VStack {
Text("hello")
Text("khawar")
}
}
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(HelloView())
Result:

How to change the size and position of popover's page in swiftUI?

I want to set the position and size of Popover page.
I tried all parameters of func popover, I think it may be related with attachmentAnchor and arrowEdge.
Here is my code:
import SwiftUI
struct ContentView : View {
#State var isPop = false
var body: some View {
VStack{
Button("Pop", action: {self.isPop = true})
.popover(isPresented: $isPop, attachmentAnchor: .point(UnitPoint(x: 20, y: 20)), arrowEdge: .top, content: {return Rectangle().frame(height: 100).foregroundColor(Color.blue)})
}
}
}
The effect I want:
Here is kind of correct configuration
struct ContentView: View {
#State private var isPop = false
#State private var text = ""
var body: some View {
VStack{
Button("Pop") { self.isPop.toggle() }
.popover(isPresented: $isPop,
attachmentAnchor: .point(.bottom), // here !
arrowEdge: .bottom) { // here !!
VStack { // just example
Text("Test").padding(.top)
TextField("Placeholder", text: self.$text)
.padding(.horizontal)
.padding(.bottom)
.frame(width: 200)
}
}
}
}
}
The attachmentAnchor is in UnitPoint, ie in 0..1 range with predefined constants, and arrowEdge is just Edge, to which popover arrow is directed. And size of popover is defined by internal content, by default it tights to minimal, but using standard .padding/.frame modifiers it can be extended to whatever needed.
One solution that I found that worked is putting padding around the button and then offset. I needed the offset to reposition the button correctly after the padding was put in. So depending on your design just a padding should work. ArrowEdge parameter and attactmentAnchor are also important but default work well for me with the popover below the button.
For example:
VStack {
Button("Pop", action: {self.isPop = true})
.padding(.bottom, 6) // add in padding to position the popover
.popover(isPresented: $isPop, attachmentAnchor: .point(UnitPoint(x: 20, y: 20)), arrowEdge: .top, content: {return Rectangle().frame(height: 100).foregroundColor(Color.blue)})
}

Resources