I am trying to create a container like view to display loader or draw background.
import SwiftUI
struct BaseView<Content: View> : View {
#State var displayLoader = false
var content: () -> Content
var body: some View {
GeometryReader { geometry in
ZStack {
Image("")
.resizable()
.aspectRatio(geometry.size, contentMode: .fill)
.edgesIgnoringSafeArea(.all)
.background(LinearGradient(gradient: Gradient(colors: [.colorDarkGray, .colorGray]), startPoint: .top, endPoint: .bottom))
LoadingView(isShowing: .constant(self.displayLoader)) {
self.content() // here i want to send parameter to inner view.
}
}
}
}
}
This is one of the inner view that i created
struct LoginOptionsView: View {
var body: some View {
BaseView {
VStack(){
VStack() {
Button(action: {
//HERE I WANT TO DISPLAY LOADER FROM PARENT VIEW like to set displayLoader to true.
}) {
Text("LOG IN").foregroundColor(Color.white)
}
.frame(minWidth: 0, idealWidth: .infinity, maxWidth: 0, minHeight: 50, idealHeight: 50, maxHeight: .none, alignment: .center)
.background(Color.colorPink)
.cornerRadius(25, antialiased: true)
}
}
}
}
I trying bindings, observable object etc but i could not send self.content a parameter which is displayLoader.
Is there a cool and easy way to do it?
If you want to have updated value in your parent view, you should not use constant binding.
// self.$displayLoader gives you the binding you want
LoadingView(isShowing: self.$displayLoader) {
self.content() // here i want to send parameter to inner view.
}
you can now send displayLoader as a binding to your LoginOptionsView and change the value.
Related
I have a custom camera view which uses UIKit to capture pictures and store it in a CameraViewModel in my SwiftUI project. The CameraPreview is what acts as the view Finder for my camera view and uses AVFoundation:
struct CameraPreview: UIViewRepresentable {
class VideoPreviewView: UIView {
override class var layerClass: AnyClass {
AVCaptureVideoPreviewLayer.self
}
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
return layer as! AVCaptureVideoPreviewLayer
}
}
let session: AVCaptureSession
func makeUIView(context: Context) -> VideoPreviewView {
let view = VideoPreviewView()
view.backgroundColor = .black
view.videoPreviewLayer.cornerRadius = 0
view.videoPreviewLayer.session = session
view.videoPreviewLayer.connection?.videoOrientation = .portrait
return view
}
func updateUIView(_ uiView: VideoPreviewView, context: Context) {
}
}
and the I use this in my CameraView.swift body as such
#StateObject var model = CameraViewModel()
#State var currentZoomFactor: CGFloat = 1.0
#Binding var showCameraView: Bool
// MARK: [main body starts here]
var body: some View {
GeometryReader { reader in
ZStack {
// This black background lies behind everything.
Color.black.edgesIgnoringSafeArea(.all)
CameraPreview(session: model.session)
.onAppear {
model.configure()
}
.alert(isPresented: $model.showAlertError, content: {
Alert(title: Text(model.alertError.title), message: Text(model.alertError.message), dismissButton: .default(Text(model.alertError.primaryButtonTitle), action: {
model.alertError.primaryAction?()
}))
})
.overlay(
Group {
if model.willCapturePhoto {
Color.black
}
}
)
.scaledToFill()
.ignoresSafeArea()
.frame(width: reader.size.width,height: reader.size.height )
// .animation(.easeInOut)
VStack {
HStack {
Button {
//
} label: {
Image(systemName: "xmark")
.resizable()
.frame(width: 20, height: 20)
.tint(.white)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
Spacer()
flashButton
}
HStack {
capturedPhotoThumbnail
Spacer()
captureButton
Spacer()
flipCameraButton
}
.padding([.horizontal, .bottom], 20)
.frame(maxHeight: .infinity, alignment: .bottom)
}
} // [ZStack Ends Here]
} // [Geometry Reader Ends here]
} // [Main Body Ends here]
I wish to open the camera View when someone presses a button somewhere on my app, like so
#State var showCamera: Bool = false
var body: some View {
mainTabView
.overlay {
CameraView(showCamera: $showCamera)
}
}
But when I do this in my app, no matter where I put the camera overlay, it stays open and the close button does nothing to close the camera view either. I'm pretty sure this is my fundamental lack of how views are constructed in UIKit and how the UIViewRepresentable works but I'd like some help regardless on how I'd achieve the desired effect. Also, any resources to understand how this works would also be greatly appreciated. Thank you.
In a MacOS app, I have a LazyVGrid. As we resize the window width, it adapts the layout, i.e. rearranges its items.
I would like to animate these layout changes. Afaik the recommended ways are withAnimation and .animation(Animation?, value: Equatable), but grid layout is automatic based on its container, it does not depend on a variable that I manage. For my use, .animation(Animation?) would be suitable, but it is now deprecated - it still works, though.
Shall I manage some state variable, e.g. a windowWidth (which I update on window-resize events), just for this animation? Or do you know a better way?
import SwiftUI
#main
struct MacosPlaygroundApp: App {
let colors: [Color] = [.red, .pink, .blue, .brown, .cyan, .gray, .green, .indigo, .orange, .purple]
let gridItem = GridItem(
.adaptive(minimum: 100, maximum: 100),
spacing: 10,
alignment: .center
)
var body: some Scene {
WindowGroup {
ScrollView(.vertical, showsIndicators: false) {
LazyVGrid(columns: [gridItem], alignment: .center, spacing: 10) {
ForEach(colors, id: \.self) { color in
color
.contentShape(Rectangle())
.cornerRadius(4)
.frame(width: 100, height: 100)
}
}.animation(.easeIn)
}
.frame(minWidth: 120, maxWidth: .infinity, minHeight: 120, maxHeight: .infinity)
}
}
}
You can make the columns as state in this view and calculate the values within the withAnimation block.
Like this:
#State var gridItems: [GridItem] = []
var body: some View {
GeometryReader { proxy in
ScrollView(.vertical) {
LazyVGrid(columns: gridItems) {
itemGrid()
}
}.onChange(of: proxy.size) { newValue in
if gridItems.isEmpty {
updateGridItems(newValue.width)
} else {
withAnimation(.easeOut(duration: 0.2)) {
updateGridItems(newValue.width)
}
}
}.onAppear {
if gridItems.isEmpty {
updateGridItems(proxy.size.width)
}
}
}
}
You can access the full example in my app in GitHub: https://github.com/JuniperPhoton/MyerTidy.Apple/blob/main/Shared/View/BatchOperationSelectView.swift#L47
I'm using a ForEach within a horizontal ScrollView to display a list of movies from my movies array. I want to make an interaction like Netflix where the background updates with the an image, movie description, etc. based on the item currently in focus.
This is the best solution I've managed to come up with so far but its very far off.
My MainMenuView:
struct MainMenuView: View {
#ObservedObject var vm = MainMenuViewModel()
var body: some View {
VStack {
Text("Movies")
.font(.title3.weight(.semibold))
.frame(maxWidth: .infinity, alignment: .leading)
ScrollView(.horizontal) {
LazyHStack {
ForEach(vm.selections) { selection in
Button {
print("Hello, World!")
} label: {
ZStack {
SelectionPreview(selection: selection)
.frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height, alignment: .bottom)
Image(selection.thumbnail)
.resizable()
.scaledToFill()
.frame(width: 500, height: 281)
}
}
.cornerRadius(32)
.buttonStyle(.plain)
}
}
}
}
}
}
My SelectionPreview:
struct SelectionPreview: View {
#State var selection: Movie
#Environment(\.isFocused) var isFocused: Bool
var body: some View {
Image(isFocused ? selection.backdrop : "")
.resizable()
.aspectRatio(contentMode: .fit)
}
}
The above code does manage to update a background based on the current item in focus but all the other items in the list get pushed to the side since the View is now the size of the background image.
I also tried to set a background on the entire VStack instead but this didn't work. I think because the Vstack on its own is not focusable:
Something like this:
struct MainMenuView: View {
var body: some View {
VStack {
ScrollView(.horizontal) {
[...]
}
}
.background(
SelectionPreview()
)
}
}
My goal is to achieve something similar to this:
Any help would be appreciated!
I am trying to create a full bleed SwiftUI 'header' view in my scene. This header will sit within a List or a scrollable VStack.
In order to do this, I'd like to have my text in the header positioned below the safe area, but the full view should extend from the top of the screen (and thus, overlap the safe area). Here is visual representation:
V:[(safe-area-spacing)-(padding)-(text)]
here is my attempt:
struct HeaderView: View {
#State var spacing: CGFloat = 100
var body: some View {
HStack {
VStack(alignment: .leading) {
Rectangle()
.frame(height: spacing)
.opacity(0.5)
Text("this!").font(.largeTitle)
Text("this!").font(.headline)
Text("that!").font(.subheadline)
}
Spacer()
}
.frame(maxWidth: .infinity)
.background(Color.red)
.background(
GeometryReader { proxy in
Color.clear
.preference(
key: SafeAreaSpacingKey.self,
value: proxy.safeAreaInsets.top
)
}
)
.onPreferenceChange(SafeAreaSpacingKey.self) { value in
self.spacing = value
}
}
}
This however, does not seem to correctly size 'Rectangle'. How can I size a view according to the safe area?
Is this what you're looking for? I try to avoid using GeometryReader unless you really need it... I created a MainView, which has a background and a foreground layer. The background layer will ignore the safe areas (full bleed) but the foreground will stay within the safe area by default.
struct HeaderView: View {
var body: some View {
HStack {
VStack(alignment: .leading) {
Text("this!").font(.largeTitle)
Text("this!").font(.headline)
Text("that!").font(.subheadline)
}
Spacer(minLength: 0)
}
}
}
struct MainView: View {
var body: some View {
ZStack {
// Background
ZStack {
}
.frame(maxWidth:. infinity, maxHeight: .infinity)
.background(Color.red)
.edgesIgnoringSafeArea(.all)
// Foreground
VStack {
HeaderView()
Spacer()
}
}
}
}
add an state to store desired height
#State desiredHeight : CGFloat = 0
then on views body :
.onAppear(perform: {
if let window = UIApplication.shared.windows.first{
let phoneSafeAreaTopnInset = window.safeAreaInsets.top
desiredHeight = phoneSafeAreaTopnInset + x
}
})
set the desiredHeight for your view .
.frame(height : desiredHeight)
I would like to be able to save and restore the position of a SwiftUI splitView but I can’t figure out how to do it. I haven’t found any examples and the documentation doesn’t have any info. I have the following:
struct ContentView: View {
var body: some View {
GeometryReader{geometry in
HSplitView(){
Rectangle().foregroundColor(.red).layoutPriority(1)
Rectangle().foregroundColor(.green).frame(minWidth:200, idealWidth: 200, maxWidth: .infinity)
}.frame(width: geometry.size.width, height: geometry.size.height)
}
}
}
Does anyone know how I can get the position of the slider so it can be saved, and also restored on startup?
Thanks!
Here's a workaround I've been using. It uses a plain HStack and a draggable view to recreate what HSplitView does. You can save then save the draggableWidth state however you want.
public struct SlideableDivider: View {
#Binding var dimension: Double
#State private var dimensionStart: Double?
public init(dimension: Binding<Double>) {
self._dimension = dimension
}
public var body: some View {
Rectangle().background(Color.gray).frame(width: 2)
.onHover { inside in
if inside {
NSCursor.resizeLeftRight.push()
} else {
NSCursor.pop()
}
}
.gesture(drag)
}
var drag: some Gesture {
DragGesture(minimumDistance: 10, coordinateSpace: CoordinateSpace.global)
.onChanged { val in
if dimensionStart == nil {
dimensionStart = dimension
}
let delta = val.location.x - val.startLocation.x
dimension = dimensionStart! + Double(delta)
}
.onEnded { val in
dimensionStart = nil
}
}
}
...
struct ContentView: View {
#AppStorage("ContentView.draggableWidth") var draggableWidth: Double = 185.0
var body: some View {
// This will be like an HSplitView
HStack(spacing: 0) {
Text("Panel 1")
.frame(width: CGFloat(draggableWidth))
.frame(maxHeight: .infinity)
.background(Color.blue.opacity(0.5))
SlideableDivider(dimension: $draggableWidth)
Text("Panel 2")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red.opacity(0.5))
}.frame(maxWidth: .infinity)
}
}