macOS: Settings window positioning - swift

I would like to position the Settings window, e.g.:
Horizontal: centered in the screen
Vertical: 200px from screen top
The code below does position it, but with some delay. It first appears where the system decides to put it, then my positioning is applied. I would like to avoid this "flash" effect.
import SwiftUI
#main
struct MacosPlaygroundApp: App {
var body: some Scene {
WindowGroup("Playground") {
Text("Hello, World!")
.frame(width: 700, height: 300, alignment: .center)
}
Settings {
SettingsView()
}
}
}
struct SettingsView: View {
#State private var selectedTab: SettingsTab = .first
var body: some View {
TabView {
Text("First settings")
.tabItem { Label("First", systemImage: "1.square") }
.tag(SettingsTab.first)
Text("Second settings")
.tabItem { Label("Second", systemImage: "2.square") }
.tag(SettingsTab.second)
}
.frame(width: 400, height: 200, alignment: .top)
.onAppear {
// I wrap it in a Task because `settingsWindow` would be `nil` otherwise
Task {
guard let settingsWindow = NSApplication.shared.windows.first(where: { $0.title == windowTitle }) else { return }
guard let screen = settingsWindow.screen else { return }
let settingsWindowSize = settingsWindow.frame.size
let screenSize = screen.frame.size
let left = (screenSize.width - settingsWindowSize.width) / 2
let bottom = screenSize.height - 200
settingsWindow.setFrameTopLeftPoint(NSPoint(x: left, y: bottom))
}
}
}
var windowTitle: String {
switch selectedTab {
case .first:
return "First"
case .second:
return "Second"
}
}
enum SettingsTab {
case first
case second
}
}

Related

Buttons inside overlay not clickable SwiftUI

I have an overlay modifier with some Buttons that I'd like to make clickable, but they are not working for some reason. It seems like a simple fix, but moving the offsets to different subviews didn't seem to work.
struct MyView: View {
#State private var textOffset = 300
let users = ["first","second","third","fourth","fifth","sixth","seventh"]
var body: some View {
Color.yellow
.frame(width: 200, height: 20)
.overlay (
HStack {
ForEach(users, id: \.self) { user in
if user == users.first {
Link(user, destination: URL(string: "myelin")!)
} else {
Text("•")
Link(user, destination: URL(string: "mylink2")!)
}
}
}
.fixedSize()
.offset(x: textoffset, y: 0)
)
.animation(.linear(duration: 10)
.repeatForever(autoreverses: false), value: textoffset)
.clipped()
.onAppear {
textoffset = -300.0
}
}
}
.verlay() adds an another layer on top of the view. We can achive this with ZStack that arrange views in z axis.
struct ContentView: View {
let users = ["first","second","third","fourth","fifth","sixth","seventh"]
var body: some View {
ZStack {
Color.yellow
ScrollView(.horizontal) {
HStack {
ForEach(users, id: \.self) { user in
if user == users.first {
Link(user, destination: URL(string: "myelin")!)
} else {
Text("•")
Link(user, destination: URL(string: "mylink2")!)
}
}
}
}
}
.frame(width: 200, height: 20)
}
}

SwiftUI View method called but output missing

I have the View AlphabetLetterDetail:
import SwiftUI
struct AlphabetLetterDetail: View {
var alphabetLetter: AlphabetLetter
var letterAnimView : LetterAnimationView
var body: some View {
VStack {
Button(action:
animateLetter
) {
Image(uiImage: UIImage(named: "alpha_be_1")!)
.resizable()
.scaledToFit()
.frame(width: 60.0, height: 120.0)
}
letterAnimView
}.navigationBarTitle(Text(verbatim: alphabetLetter.name), displayMode: .inline)
}
func animateLetter(){
print("tapped")
letterAnimView.timerWrite()
}
}
containing the View letterAnimView of Type LetterAnimationView:
import SwiftUI
struct LetterAnimationView: View {
#State var Robot : String = ""
let LETTER =
["alpha_be_1_81",
"alpha_be_1_82",
"alpha_be_1_83",
"alpha_be_1_84",
"alpha_be_1_85",
"alpha_be_1_86",
"alpha_be_1_87",
"alpha_be_1_88",
"alpha_be_1_89",
"alpha_be_1_90",
"alpha_be_1"]
var body: some View {
VStack(alignment:.center){
Image(Robot)
.resizable()
.frame(width: 80, height: 160, alignment: .center)
.onAppear(perform: timerWrite)
}
}
func timerWrite(){
var index = 0
let _ = Timer.scheduledTimer(withTimeInterval: 0.08, repeats: true) {(Timer) in
Robot = LETTER[index]
print("one frame")
index += 1
if (index > LETTER.count - 1){
Timer.invalidate()
}
}
}
}
This gives me a fine animation, as coded in func timerWrite() and performed by .onAppear(perform: timerWrite).
After commenting //.onAppear(perform: timerWrite) I try animating by clicking
Button(action: animateLetter)
but nothing happens.
Maybe I got two different instances of letterAnimView, if so why?
Can anybody of you competent guys intentify my mistake?
Regards - Klaus
You don't want to store Views, they are structs so they are copied. Instead, create an ObservableObject to encapsulate this functionality.
I created RobotModel here with other minor changes:
class RobotModel: ObservableObject {
private static let atlas = [
"alpha_be_1_81",
"alpha_be_1_82",
"alpha_be_1_83",
"alpha_be_1_84",
"alpha_be_1_85",
"alpha_be_1_86",
"alpha_be_1_87",
"alpha_be_1_88",
"alpha_be_1_89",
"alpha_be_1_90",
"alpha_be_1"
]
#Published private(set) var imageName: String
init() {
imageName = Self.atlas.last!
}
func timerWrite() {
var index = 0
let _ = Timer.scheduledTimer(withTimeInterval: 0.08, repeats: true) { [weak self] timer in
guard let self = self else { return }
self.imageName = Self.atlas[index]
print("one frame")
index += 1
if index > Self.atlas.count - 1 {
timer.invalidate()
}
}
}
}
struct AlphabetLetterDetail: View {
#StateObject private var robot = RobotModel()
let alphabetLetter: AlphabetLetter
var body: some View {
VStack {
Button(action: animateLetter) {
Image(uiImage: UIImage(named: "alpha_be_1")!)
.resizable()
.scaledToFit()
.frame(width: 60.0, height: 120.0)
}
LetterAnimationView(robot: robot)
}.navigationBarTitle(Text(verbatim: alphabetLetter.name), displayMode: .inline)
}
func animateLetter() {
print("tapped")
robot.timerWrite()
}
}
struct LetterAnimationView: View {
#ObservedObject var robot: RobotModel
var body: some View {
VStack(alignment:.center){
Image(robot.imageName)
.resizable()
.frame(width: 80, height: 160, alignment: .center)
.onAppear(perform: robot.timerWrite)
}
}
}

Disable animation when a view appears in SwiftUI

I got a problem while trying to display a custom loading view in SwiftUI.
I created a custom struct view OrangeActivityIndicator:
struct OrangeActivityIndicator: View {
var style = StrokeStyle(lineWidth: 6, lineCap: .round)
#State var animate = false
let orangeColor = Color.orOrangeColor
let orangeColorOpaque = Color.orOrangeColor.opacity(0.5)
init(lineWidth: CGFloat = 6) {
style.lineWidth = lineWidth
}
var body: some View {
ZStack {
Circle()
.trim(from: 0, to: 0.7)
.stroke(
AngularGradient(gradient: .init(colors: [orangeColor, orangeColorOpaque]), center: .center), style: style
)
.rotationEffect(Angle(degrees: animate ? 360 : 0))
.animation(Animation.linear(duration: 0.7).repeatForever(autoreverses: false))
}.onAppear() {
self.animate.toggle()
}
}
}
I use it inside different screens or views, my problem is that it appears weirdly, for example in CampaignsView of the app I display it when the server call is in progress.
struct CampaignsView: View {
#ObservedObject var viewModel: CampaignsViewModel
var body: some View {
NavigationView {
ZStack {
VStack(spacing: 0) {
CustomNavigationBar(campaignsNumber: viewModel.cardCampaigns.count)
.padding([.leading, .trailing], 24)
.frame(height: 25)
CarouselView(x: $viewModel.x, screen: viewModel.screen, op: $viewModel.op, count: $viewModel.index, cardCampaigns: $viewModel.cardCampaigns).frame(height: 240)
CampaignDescriptionView(idx: viewModel.index, cardCampaigns: viewModel.cardCampaigns)
.padding([.leading, .trailing], 24)
Spacer()
}
.onAppear {
self.viewModel.getCombineCampaigns()
}
if viewModel.isLoading {
OrangeActivityIndicator()
.frame(width: 40, height: 40)
}
}
.padding(.top, 34)
.background(Color.orBackgroundGrayColor.edgesIgnoringSafeArea(.all))
.navigationBarHidden(true)
}
}
}
The Indicator itself is correctly spinning, the problem is when it appears, it appears as a translation animation coming from the bottom to the middle of the screen. This is my viewModel with the server call and isLoading property:
class CampaignsViewModel: ObservableObject {
#Published var index: Int = 0
#Published var cardCampaigns: [CardCampaign] = [CardCampaign]()
#Published var isLoading: Bool = false
var cancellable: AnyCancellable?
func getCombineCampaigns() {
self.isLoading = true
let campaignLoader = CampaignLoader()
cancellable = campaignLoader.getCampaigns()
.receive(on: DispatchQueue.main)
//Handle Events operator is used for debugging.
.handleEvents(receiveSubscription: { print("Receive subscription: \($0)") },
receiveOutput: { print("Receive output: \($0)") },
receiveCompletion: { print("Receive completion: \($0)") },
receiveCancel: { print("Receive cancel") },
receiveRequest: { print("Receive request: \($0)") })
.sink { completion in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { campaignResult in
self.isLoading = false
guard let campaignsList = campaignResult.content else {
return
}
self.cardCampaigns = campaignsList.map { campaign in
return CardCampaign(campaign: campaign)
}
self.moveToFirstCard()
}
}
}
Use animation with linked related state
var body: some View {
ZStack {
Circle()
.trim(from: 0, to: 0.7)
.stroke(
AngularGradient(gradient: .init(colors: [orangeColor, orangeColorOpaque]), center: .center), style: style
)
.rotationEffect(Angle(degrees: animate ? 360 : 0))
.animation(Animation.linear(duration: 0.7)
.repeatForever(autoreverses: false),
value: animate) // << here !!
}.onAppear() {
self.animate.toggle()
}
}
SwiftUI 3
(iOS 15)
use the new API
.animation(.default, value: isAppeared)
SwiftUI 1, 2
Like below, when I first load a view, the button was animated.
So, I used the GeometryReader.
The animation is set to nil while the coordinates of the dummy are changing.
RecordView.swift
struct RecordView: View {
var body: some View {
Button(action: { }) {
Color(.red)
.frame(width: 38, height: 38)
.cornerRadius(6)
.padding(34)
.overlay(Circle().stroke(Color(.red), lineWidth: 8))
}
.buttonStyle(ButtonStyle1()) // 👈 customize the button.
}
}
ButtonStyle.swift
import SwiftUI
struct ButtonStyle1: ButtonStyle {
// In my case, the #State isn't worked, so I used class.
#ObservedObject private var data = ButtonStyle1Data()
func makeBody(configuration: Self.Configuration) -> some View {
ZStack {
// Dummy for tracking the view status
GeometryReader {
Color.clear
.preference(key: FramePreferenceKey.self, value: $0.frame(in: .global))
}
.frame(width: 0, height: 0)
.onPreferenceChange(FramePreferenceKey.self) { frame in
guard !data.isAppeared else { return }
// ⬇️ This is the key
data.isLoaded = 0 <= frame.origin.x
}
// Content View
configuration.label
.opacity(configuration.isPressed ? 0.5 : 1)
.scaleEffect(configuration.isPressed ? 0.92 : 1)
.animation(data.isAppeared ? .easeInOut(duration: 0.18) : nil)
}
}
}
ButtonStyleData.swift
final class ButtonStyle1Data: ObservableObject {
#Published var isAppeared = false
}
FramePreferenceKey.swift
struct FramePreferenceKey: PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {}
}
Result

How to present modal in SwiftUI automatically without button

I am thinking of using a switch case to present different views.
struct searchview : View {
var body: some View {
VStack {
VStack {
if self.speechRecognition.isPlaying == true {
VStack {
Text(self.speechRecognition.recognizedText).bold()
.foregroundColor(.white)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.font(.system(size: 50))
.sheet(isPresented: $showSheet) {
self.sheetView
}
}.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.showSheet = true
}
}
}
}.onAppear(perform: getViews)
}
var currentSheetView: String {
var isProductDictEmpty = Global.productDict.isEmpty
var wasTextRecognizedEmpty = self.speechRecognition.recognizedText.isEmpty
var checkTextandDict = (wasTextRecognizedEmpty,isProductDictEmpty)
switch checkTextandDict {
case (true,true):
print("Product dict and Text rec are empty")
return "error"
case (false,false):
print("Yes we are in business")
return "product"
case (true,false):
print("OOPS we didnt catch that")
return "error"
case (false,true):
print("OOPS we didnt catch that")
return "zero match"
}
}
#ViewBuilder
var sheetView: some View {
if currentSheetView == "product" {
ProductSearchView(model: self.searchModel)
}
else if currentSheetView == "zero match" {
zeroResult()
}
else if currentSheetView == "error" {
SearchErrorView()
}
}
}
}
I know how to use .sheet modifier to present modal when a button is pressed but how could I the present the respective modals in swift cases automatically with button?
UPDATE
these are the views I am trying to implement
struct SearchErrorView: View {
var body: some View {
VStack {
Text("Error!")
Text("Oops we didn't catch that")
}
}
}
struct zeroResult: View {
var body: some View {
Text("Sorry we did not find any result for this item")
}
}
I'm not sure what I am doing wrongly. I tried to implement the solution below but still not able to call the views with the switch cases.
The solution is to programmatically set a variable controlling displaying of your sheet.
You can try the following to present your sheet in onAppear:
struct ContentView: View {
#State var showSheet = false
var body: some View {
Text("Main view")
.sheet(isPresented: $showSheet) {
self.sheetView
}
.onAppear {
self.showSheet = true
}
}
var currentSheetView: String {
"view1" // or any other...
}
#ViewBuilder
var sheetView: some View {
if currentSheetView == "view1" {
Text("View 1")
} else {
Text("View 2")
}
}
}
You may delay it as well:
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.showSheet = true
}
}
Just setting self.showSheet = true will present the sheet. It's up to you how you want to trigger it.

SwiftUI - How to limit the scope of animation to only the onAppear Transition

I'm new to SwiftUI and working through some sample projects to get the hang of it and I'm getting stuck on limiting the scope of the animation I set for the .transition for an AnimationModifier so it only impacts the animation of the transition and nothing else in the view.
While the separate transitions are respected for onAppear() and another for onDisappear(). The animation in the AnimatableModifier is overriding the removal over the item from the grid even when explicitly declared
I've tried explcicitly setting the Animation to the .offset transition in both the AnimatableModifier and for the CardView in the GameView, and when I do, no animation is triggered at all:
.transition(AnyTransition.offset(CGSize.init(width: randomXLocation, height: -offset.height-50)).animation(Animation.easeInOut(duration: 1.25).delay(delay)))
So, there's gotta be a way to limit the scope or explicitly declare the animation for transition or two separate Animations in the Animation Modifier, but I'm not finding any resources on how to move forward.
GameView.swift
struct GameView: View {
#ObservedObject var viewModel: SetGameViewModel
#State var delay: Double = 0.1
var body: some View {
GeometryReader { geometry in
VStack {
Grid(newItems: self.viewModel.newCards,
items: self.viewModel.cards.itemsAtWithIds(ids: self.viewModel.idOfCardsToDisplay)) { card in
CardView(card: card, bodyGeoProxy: geometry, delay: self.delay).onTapGesture {
self.viewModel.choose(card: card)
}
.transition(AnyTransition.offset(CGSize.init(width: randomXLocation, height: -offset.height-50)))
.animation(Animation.easeInOut(duration: 1.25).delay(delay))
.onAppear() {
let maxDelay: Double = Double(self.viewModel.cards.itemsAtWithIds(ids: self.viewModel.idOfCardsToDisplay).count)*0.2 + 0.2
if self.delay < 2.5 {
self.delay = self.delay + 0.2
} else if self.delay >= maxDelay {
self.delay = 0.1
}
}
}
HStack{
Button(action: {
self.viewModel.dealThreeCards()
}) {
Text("Hit Me")
}
Spacer()
Text("Score: \(self.viewModel.score)")
Spacer()
Button(action: {
self.viewModel.dealThreeCards()
}) {
Text("New Game")
}
}
}
}
}
}
GameView.swift
struct CardView: View{
var card: SetGame<SoloSetCardContent>.Card
var bodyGeoProxy: GeometryProxy
var delay: Double
var body: some View {
GeometryReader { geometry in
self.body(for: geometry)
}
}
init(card: SetGame<SoloSetCardContent>.Card, bodyGeoProxy: GeometryProxy, delay: Double) {
self.card = card
self.bodyGeoProxy = bodyGeoProxy
self.delay = delay
}
#ViewBuilder
func body(for geometryProxy: GeometryProxy) -> some View {
ZStack {
if card.isSelected {
RoundedRectangle(cornerRadius: 5)
.fill(Color.gray)
.frame(width: geometryProxy.size.width-4, height: geometryProxy.size.height-4, alignment: .center)
.border(Color.blue, width: 2)
.animation(nil)
} else {
RoundedRectangle(cornerRadius: 5)
.fill(Color.gray)
.frame(width: geometryProxy.size.width-4, height: geometryProxy.size.height-4, alignment: .center)
.border(Color.red, width: 2)
.animation(nil)
}
VStack {
ForEach(0..<self.card.content.deckShapes.count) { index in
VStack {
Spacer(minLength: 5)
ShapeView(setShape: self.card.content.deckShapes[index])
.frame(width: (geometryProxy.size.width-geometryProxy.size.width/5), height: geometryProxy.size.height/5, alignment: .center)
Spacer(minLength: 5)
}
}
}
}
.deal(delay: self.delay, offset: bodyGeoProxy.size)
}
}
Dealer.Swift - AnimatableModifier
struct Dealer: AnimatableModifier {
#State var show: Bool = false
var delay: Double
var offset: CGSize
var randomXLocation: CGFloat {
CGFloat.random(in: -offset.width ..< offset.width)
}
func body(content: Content) -> some View {
ZStack {
if show {
content
.transition(AnyTransition.offset(CGSize.init(width: randomXLocation, height: -offset.height-450)))
.animation(Animation.easeInOut(duration: 1.25).delay(delay))
}
}
.onAppear {
withAnimation {
self.show = true
}
}
.onDisappear {
withAnimation {
self.show = false
}
}
}
}
extension View {
func deal(delay: Double, offset: CGSize) -> some View {
self.modifier(Dealer(delay: delay, offset: offset))
}
}
I was able to resolve this by removing the Animation from the body content (and elsewhere) and adding to withAnimation portion of the .onAppear method in body function of the AnimationModifier
func body(content: Content) -> some View {
ZStack {
if show {
content
.transition(.asymmetric(insertion: .offset(CGSize.init(width: randomXLocation, height: -offset.height-50)),
removal: .offset(CGSize.init(width: randomXLocation, height: offset.height+50))))
}
}
.onDisappear {
withAnimation (Animation.easeInOut(duration: 1.25).delay(0)) {
self.show = false
}
}
.onAppear {
withAnimation (Animation.easeInOut(duration: 1.25).delay(self.delay)) {
self.show = true
}
}
}