TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

Is there any help for a new swift user/forum member that need to retrieve data from another swift file?

Forums > SwiftUI

@Zenki  

Hi all, i'm new here. And also in swift. I don't want to become a dev, nor learn swift in deep because i'm not planning to make another app than the one i'm working on. Starting with this, i'm sure i've lost the most of you... I got an M2 Apple Sillicon last year, and the notch is really cool as it is, bringing more spaces to the screen, but it need some fun! I've decided maybe 3 months ago to make a simple app that show battery level around the notch, displaying color gradually the more the battery level decrease. Was a pain for someone like me... But it's there, it work. I can leave it as is. But i'm a perfectionniste, and i want to made some changes and improvements. I would appreciate if i could find any help here. I've started with an app called TheNotch on github. With help of AI, i've made the color animation, battery level and powersource monitor. But after about a month of searches, i couldn't find my way. There's many things i couldn't achieve alone, my AI assisstant didn't help me in these cases... make me think artificial intelligence is not so intelligent than that... :) First of all, i know my code could/should be cleaner... Be lenient please. But you can help improving it!! :)

1) Trying to be clear, i have stroke of RoundedRectangle that animate at start with progress, representing the battery level. A second stroke with progress too behind representing the background, which make the notch more visible (color.white.opacity(0.2)). The animation works well, start from 0 to 0.5 as it represent the half of the RoundedRectangle. What i want is the animation go back to 0 when i press the button that close the app, then close the app. The button is in another swift file (ConfigView). But when i click on the close app button, nothing. Just a delay (DispatchQueue). AI said it's because it didn't act on the same instance of progress... hum....?! I'm not familiar with that, i don't know what to do now. The answers AI gave me doesn't work, or at least i couldn't make them works in my project. He don't understand "the button is on another swift file"... Here is my code:

import SwiftUI
import IOKit.ps
import Combine

struct NotchView: View {
    var body: some View {
        ZStack{
            BatteryView() 
            //if BatteryView().isPlugged { MyView() }
        }
        .onTapGesture {
            TheNotchApp.singleton?.window?.makeKeyAndOrderFront(self)
        }
        .mask(RoundedRectangle(cornerRadius: 12))
    }
}

struct BatteryView : View {
    @ObservedObject var powerSourceMonitor = PowerSourceMonitor()
    @State private var batteryLevel = Double(getLevel() / 100)/2
    @State private var value = getLevel()
    @State var isPlugged: Bool = false
    @State var progress: Double = 0.0

  var body: some View {
      ZStack{
          RoundedRectangle(cornerRadius: 8)
              .padding(5)
              .foregroundColor(Color.black)

          RoundedRectangle(cornerRadius: 15)
              .trim(from: 0, to: CGFloat(min(progress, 0.5)))
              .stroke(style: StrokeStyle(lineWidth: 9))
              .foregroundColor(isPlugged ? getColor(value).opacity(0.4) : Color.white.opacity(0.2))
              .animation(.easeOut(duration: 1.0), value: progress)
              .animation(.linear(duration: 0.2).repeatCount(3, autoreverses: true), value: isPlugged)

          RoundedRectangle(cornerRadius: 15)
              .trim(from: 0, to: CGFloat(min(progress, batteryLevel)))
              .stroke(style: StrokeStyle(lineWidth: 9))
              .foregroundColor(getColor(value))
              .animation(.easeOut(duration: 1.0), value: progress)

      } //ZStack
      .frame(width: 218, height: 82)
      .background(Color.clear)
      .scaleEffect(x: -1, y: 1)
      .onReceive(powerSourceMonitor.$powerSourceChanged) { _ in
          isPlugged = powerSourceMonitor.checkPowerStatus()
      }
      .onAppear{
          startTimer()
          progress = 0.5
      }

  }  //Body

  func startTimer() {
      @State var setColour = getColor(value)
      DispatchQueue.main.asyncAfter(deadline: .now() + 180) {
          batteryLevel = Double(getLevel() / 100)/2
          setColour = getColor(value)
          startTimer()
      }
  }

  private func getColor(_ value: Double) -> Color {
      let red: Double
      let green: Double
      let blue: Double

      if value <= 40 {
          red = 1.0
          green = value / 40
          blue = 0.0
      } else if value <= 50 {
          red = 1.0 - (value - 40) / 10
          green = 1.0
          blue = (value - 40) / 40
      } else {
          red = 0.0
          green = 1.0
          blue = 0.0
      }

      return Color(red: red, green: green, blue: blue)
  }

} //Struct BatteryView

private func getLevel() -> Double {
    let powerSourceInfo = IOPSCopyPowerSourcesInfo().takeRetainedValue()
    let powerSources = IOPSCopyPowerSourcesList(powerSourceInfo).takeRetainedValue() as Array

    for powerSource in powerSources {
        let powerSourceInfo = IOPSGetPowerSourceDescription(powerSourceInfo, powerSource).takeUnretainedValue() as! [String: Any]
        if let currentCapacity = powerSourceInfo[kIOPSCurrentCapacityKey] as? Double{
            return currentCapacity
        }
    }
    return 0
}

class PowerSourceMonitor: ObservableObject {
    @Published var powerSourceChanged: Bool = false
    init() {
        let opaque = Unmanaged.passRetained(self).toOpaque()
        let context = UnsafeMutableRawPointer(opaque)
        let loop: CFRunLoopSource = IOPSNotificationCreateRunLoopSource(
            PowerSourceChanged,
            context
        ).takeRetainedValue() as CFRunLoopSource
        CFRunLoopAddSource(CFRunLoopGetCurrent(), loop, CFRunLoopMode.defaultMode)
    }
    deinit {
        Unmanaged.passUnretained(self).release()
    }
    func checkPowerStatus() -> Bool {
        let timeRemaining: CFTimeInterval = IOPSGetTimeRemainingEstimate()
        if timeRemaining == kIOPSTimeRemainingUnlimited {
            return true
        } else {
            return false
        }
    }
}

func PowerSourceChanged(context: UnsafeMutableRawPointer?) {
    let opaque = Unmanaged<PowerSourceMonitor>.fromOpaque(context!)
    let _self = opaque.takeUnretainedValue()
    DispatchQueue.main.async {
        _self.powerSourceChanged.toggle()
    }
}

struct MyView: View {
    var body: some View {
        ZStack{
            RoundedRectangle(cornerRadius: 25)
                .background(Color.blue)
            Text("Plugged In")
                .foregroundColor(Color.white)
                .background(Color.blue)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        NotchView()
    }
}

The button is in the prefpane, in another swift file.

2) Not related to the first one. Powersource monitor detect if i'm plugged in or not. All i want here is a small pill like on ios or dynamicIsland (roundedrectangle with high radius), dropping from the notch, that show a simple text saying "plugged in", but it's hidden/don't work and i don't know why...

3) Maybe related to the second. Something is wrong with the code (the same on the original) i think as i couldn't expand the NotchView on onHover for example to its both sides, it expand only from right side (trailing? but leading in this case)... I'm pretty sure it comes from here: let pos = CGPoint(x: screenWidth/2 - notchWidth/2, y: screenHeight - 42) self.setFrameOrigin(pos)

Here's the code:

import AppKit
import SwiftUI
import Cocoa
import IOKit

class NotchWindow : NSWindow {

    static var notchHeight = 264
    static var notchWidth = 218

    static var singleton: NotchWindow? = nil

    static var allowMoving = false
    static var showOnAllSpaces = false
    static var showInDock = false

    override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) {
        super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag)

        let window = self
        NotchWindow.singleton = window

        window.isOpaque = false
        window.hasShadow = false
        window.showsToolbarButton = false
        window.backgroundColor = NSColor.clear

        window.canBecomeVisibleWithoutLogin = true
        window.titleVisibility = .hidden
        window.styleMask = .borderless
        window.level = .screenSaver

        refreshNotch()
    }

    func refreshNotch() {

        // update how it can be interacted with
        self.ignoresMouseEvents = !NotchWindow.allowMoving
        self.collectionBehavior = .stationary

        if NotchWindow.showOnAllSpaces {
            self.collectionBehavior = [ self.collectionBehavior, .canJoinAllSpaces ]
        }

        var screenWidth = 1440
        var screenHeight = 900

        let notchWidth = NotchWindow.notchWidth
        //let notchHeight = NotchWindow.notchHeight
        //let notchOffset = 10

        NSApp.setActivationPolicy(.accessory)

        // As we aren't showing in the dock, it has to remain clickable
        self.ignoresMouseEvents = false
        // always show preferences, there's no other entrypoint
        NSApp.activate(ignoringOtherApps: true)

        // get window's screen dimensions, or default if they aren't available for some reason
        let frame = self.screen?.frame ?? NSRect(x: 0, y: 0, width: screenWidth, height: screenHeight)

        // get the current window's screen and its width and heights
        screenWidth = Int(frame.width)
        screenHeight = Int(frame.height)

        let pos = CGPoint(x: screenWidth/2 - notchWidth/2, y: screenHeight - 42)
        self.setFrameOrigin(pos)
        self.contentView = NSHostingView(rootView: NotchView())

    }
}

Thanks for reading. Thanks for all, even if no help!!

3      

@Zenki  

Hi, is there something wrong with my questions? Is there too many questions at same time? I've tryed this for the first question, it keep asking me for argument: "Missing argument for parameter 'progresss' in call" in my ConfigView_Previews but i don't know what to put... All my attempts returns me errors or don't work (no error but no animation). Can anyone tell me if i'm doing it well?

I put this on my ConfigView file, which contains the button: struct ConfigView: View { @ObservedObject var progress: UserProgress

var body: some View {
    Button("Animate") {
        progress.score = 0.0
    }
}

}

this on top of my NotchView file, which contains BatteryView struct and the progress i want to animate from the button:

class UserProgress: ObservableObject { @Published var score: Double = 0.0 }

and inside BatteryView():

@StateObject var progress = UserProgress()

edited every "progress" in trim and animation to "progress.score".

It then ask for argument in call... I'm missing something? Can you tell me what to put inside the parentheses?? I've tryed "progress: UserProgress()", no error but no anim on click... only on start.

Please just tell me if i'm doing it well. Thanks.

3      

In a few other posts, I offered advice to break a big problem into many smaller problems to solve one at a time. This concept is taught in some engineering courses as "How to Eat an Elephant"

See -> How to Eat an Elephant

Try this. Create a new project. Remove all your color coding, spacing, and visual effects.

Architect your solution so that each functional requirement is solved in its own Swift class or struct. Solve one element at a time. Don't work on others until you've solved the current one.

For example, these are two different bites of your elephant:

...[SNIP]... displaying color gradually the more the battery level decrease ....[SNIP].....

You could have ONE struct that represents the battery. What is its maxium value? What is its current value. Calculate the percentage of remaining battery life. Calculate percentage when user should be alerted. Calculate battery drain per minute.

These are all properties that you want to know about your battery. All values. No visuals. SOLVE THIS PROBLEM on its own.

Later, you want to display color coding to represent these values. Develop a BatteryView object that is independent of the Battery struct. Your BatteryView should only ask the Battery for its values. The BatteryView should NOT do any calculations. Leave those rules in the Battery object.

When you finish these two bites, then figure out your next bite at the elephant.

Is there something wrong with my questions? Is Are there too many questions at same time?

Yes!

Keep coding

3      

@Zenki  

Sorry for the late reply. Thanks for yours @Obelix (may be you're french...?!). I just want add that english is not my native language.... I'm french. I understand, too many questions... Sorry for my dumbness...

With your wisdom Obé, i got the second question to work. I started from zero as you said. There's still a "mask" that hide everything is outside and i don't know why... But i have just enough space to display a "pill".

You could have ONE struct that represents the battery. What is its maxium value? What is its current value. Calculate the percentage of remaining battery life. Calculate percentage when user should be alerted. Calculate battery drain per minute. These are all properties that you want to know about your battery. All values. No visuals. SOLVE THIS PROBLEM on its own.

I'm not sure to understand well. If i create a Battery struct and add it to NotchView as Battery() in a ZStack, i cannot create a pill, this is where a mask hides everything is outside the notch! I don't need calculations. A simple if getLevel() < 20 {... should be good. Now, on hover, it display a pill that show battery percentage. That's it, it's enough for me.

I've followed your tips by getting calculations out of the NotchView like this, is this good?:

class ProgressValues: ObservableObject {
    @Published var batteryLevel = Double(getLevel() / 100)/2
    @Published var progress: Double = 0.0
    @Published var value = getLevel()
}

struct NotchView: View {
    @ObservedObject var powerSourceMonitor = PowerSourceMonitor()
    @ObservedObject var evo = ProgressValues()
    @State var isPlugged: Bool = false
    @State var isOn: Bool = false

  var body: some View {
      ZStack{ //...

The last one is in startTimer(), it still has batteryLevel = Double(getLevel() / 100)/2. To be honnest i don't know how to call it without calculation...

Back to my first and last remaining question. I couldn't get the animation to work. Even if i add ConfigView() to NotchView(), call of progress from the same file doesn't work... In ConfigView(), i've tryed @ObservedObject var evo = ProgressValues()and @State var evo = ProgressValues() none of them works...

Thanks.

3      

I have not tested this and am away from my setup. But this is off the top of my head....

Create a Battery object and put ALL BATTERY logic here. This is ONE bite of the elephant. What do you need to know about a battery in your app? Do not think about how you view it. Focus on what you need. Then use this object in a view as needed.

// Keep all your Battery logic in here.
// In your view, just declare which of these variables you want to show in the pill view.
struct Battery: Identifiable {
   var id: UUID()             // make it unique
   var maximumPowerTime: Int  // battery can run for 120 minutes.  This may change depending on load.
   var currentPowerTime: Int  // battery has 65 minutes remaining. This may change depending on load.
   var warningLevel = 20      // when do you alert the user?  20% remaining? 10%?

   // calculate percentage
   var percentRemaining: Int {
      // guard maximumPowerTime > 0 else { return 0 }  <-- Check your maths
       100 * (currentPowerTime / maximumPowerTime) 
   }

   // calculate time remaining
   var timeRemaining: Int {
       // guard  maximumPowerTime > currentPowerTime else { return 0 }  // <-- make sure your values are within tolerances.
       maximumPowerTime - currentPowerTime
   }

   // should we alert the user?  Use this in views to show red or green battery level colors.
   var isBatteryLevelOK: Bool {
       percentRemaining < warningLevel ? false : true
   }

   var isBatteryLevelCritical: Bool {
       percentRemaining < 7 ? true : false  // When is battery level CRITICAL?
   }

This answer doesn't cover your View issues. This is just a small side note to help you clear up your logic. If you put all your logic in smaller components, it makes building your views so much easier.

Maybe you don't want this as a struct? If you need to observe these values in several views, consider making this a class instead. This is where you as the solution architect need to make strategic decisions.

Keep coding!

3      

Also, please edit your post an put all code inside the markup code structure. This makes your code stand out from your questions and comments. Grazie! Merci!

// Put your code inside of markup code blocks.
struct SomeStructure: View {
    var someIntVariable: Int
    var someBoolVariable: Bool
    // More code here.....
}

3      

@Zenki  

Thanks. Is this a good start?

import SwiftUI
import IOKit.ps
import Combine

class Battery: ObservableObject {
    @Published var isPlugged: Bool = false {
        willSet {
            objectWillChange.send()
        }
    }
    @Published var isOn: Bool = false {
        willSet {
            objectWillChange.send()
        }
    }
    @Published var batteryLevel = Double(getLevel() / 100)/2 {
        willSet {
            objectWillChange.send()
        }
    }
    @Published var progress: Double = 0.0 {
        willSet {
            objectWillChange.send()
        }
    }
    @Published var value = getLevel(){
        willSet {
            objectWillChange.send()
        }
    }
    var warningLevel = 20

    var isLevelOK: Bool {
        Int(value) < warningLevel ? false : true
    }
    var isCritical: Bool {
        Int(value) < 7 ? true : false
    }

    func getColor(_ value: Double) -> Color {
        let red: Double
        let green: Double
        let blue: Double

        if value <= 40 {
            red = 1.0
            green = value / 40
            blue = 0.0
        } else if value <= 50 {
            red = 1.0 - (value - 40) / 10
            green = 1.0
            blue = (value - 40) / 40
        } else {
            red = 0.0
            green = 1.0
            blue = 0.0
        }
        return Color(red: red, green: green, blue: blue)
    }
}

struct NotchView: View {
    @ObservedObject var powerSourceMonitor = PowerSourceMonitor()
    @StateObject var evo = Battery()

        var body: some View {
            ZStack{
                RoundedRectangle(cornerRadius: 8)
                    .padding(5)
                    .foregroundColor(Color.black)

                RoundedRectangle(cornerRadius: 15)
                    .trim(from: 0, to: CGFloat(min(evo.progress, 0.5)))
                    .stroke(style: StrokeStyle(lineWidth: 9))
                    .foregroundColor(evo.isPlugged ? evo.getColor(evo.value).opacity(0.4) : Color.white.opacity(0.2))
                    .animation(.easeOut(duration: 1.0), value: evo.progress)
                    .animation(.linear(duration: 0.2).repeatCount(3, autoreverses: true), value: evo.isPlugged)

                RoundedRectangle(cornerRadius: 15)
                    .trim(from: 0, to: CGFloat(min(evo.progress, evo.batteryLevel)))
                    .stroke(style: StrokeStyle(lineWidth: 9))
                    .foregroundColor(evo.getColor(evo.value))
                    .animation(.easeOut(duration: 1.0), value: evo.progress)
            }
            .frame(width: CGFloat(NotchWindow.notchWidth), height: CGFloat(NotchWindow.notchHeight))
            .mask(RoundedRectangle(cornerRadius: 12))
            .background(Color.clear)
            .scaleEffect(x: -1, y: 1)
            .onReceive(powerSourceMonitor.$powerSourceChanged) { _ in
                evo.isPlugged = powerSourceMonitor.checkPowerStatus()
            }
            .onAppear{
                startTimer()
                evo.progress = 0.5
                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                    evo.isOn = true
                    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                        evo.isOn = false
                    }
                }
            }
            .onChange(of: evo.isPlugged) { newValue in
                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                    evo.isOn = true
                    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                        evo.isOn = false
                    }
                }
            }
            .onChange(of: getLevel()) { newLevel in
                evo.value = newLevel
            }
            .onHover{ isHovered in
                evo.isOn = isHovered
            }
            .onTapGesture {
                TheNotchApp.singleton?.window?.makeKeyAndOrderFront(self)
            }

            ZStack{
                RoundedRectangle(cornerRadius: 50)
                    .foregroundColor(.black)
                    .frame(width: 120, height: 40)
                HStack{
                    Text(evo.isPlugged ? "􀢋" : "􀛨")
                        .foregroundColor(evo.isLevelOK ? .white : .red)
                        .frame(maxHeight: 20, alignment: .center)
                        .font(.system(size: 20))
                        .animation(.linear(duration: 0.7), value: evo.isPlugged)
                    Text("\(Int(evo.value))%")
                        .foregroundStyle(evo.getColor(evo.value))
                        .animation(.linear(duration: 0.7), value: evo.value)
                }
                .offset(y: -1)
                .frame(width: evo.isCritical ? 140 : 120, height: evo.isCritical ? 50 : 40)
            }
            .opacity(evo.isOn ? 1 : 0)
            .offset(y: evo.isOn ? 0 : -50)
            .animation(.linear(duration: 0.3), value: evo.isOn)

            ZStack{
                RoundedRectangle(cornerRadius: 50)
                    .foregroundColor(.black)
                    .frame(width: 200, height: 60)
                VStack{
                    Text("Alert!! Alert!! Alert!!")
                    Text("Branchez votre Chargeur!")
                        .frame(maxWidth: 180, alignment: .center)
                        .foregroundColor(.red)
                }
                .font(.system(size: 14))
            }
            .opacity(evo.isCritical ? 1 : 0)
            .offset(y: evo.isCritical ? -47 : -80)
            .animation(.linear(duration: 0.3), value: evo.isCritical)
        }
    func startTimer() {
        @State var setColour = evo.getColor(evo.value)
        DispatchQueue.main.asyncAfter(deadline: .now() + 20) {
            evo.batteryLevel = Double(getLevel() / 100)/2
            evo.value = getLevel()
            setColour = evo.getColor(evo.value)
            startTimer()
        }
    }
}   //NotchView Struct

private func getLevel() -> Double {
    let powerSourceInfo = IOPSCopyPowerSourcesInfo().takeRetainedValue()
    let powerSources = IOPSCopyPowerSourcesList(powerSourceInfo).takeRetainedValue() as Array

    for powerSource in powerSources {
        let powerSourceInfo = IOPSGetPowerSourceDescription(powerSourceInfo, powerSource).takeUnretainedValue() as! [String: Any]
        if let currentCapacity = powerSourceInfo[kIOPSCurrentCapacityKey] as? Double{
            return currentCapacity
        }
    }
    return 0
}

class PowerSourceMonitor: ObservableObject {
    @Published var powerSourceChanged: Bool = false
    init() {
        let opaque = Unmanaged.passRetained(self).toOpaque()
        let context = UnsafeMutableRawPointer(opaque)
        let loop: CFRunLoopSource = IOPSNotificationCreateRunLoopSource(
            PowerSourceChanged,
            context
        ).takeRetainedValue() as CFRunLoopSource
        CFRunLoopAddSource(CFRunLoopGetCurrent(), loop, CFRunLoopMode.defaultMode)
    }
    deinit {
        Unmanaged.passUnretained(self).release()
    }
    func checkPowerStatus() -> Bool {
        let timeRemaining: CFTimeInterval = IOPSGetTimeRemainingEstimate()
        if timeRemaining == kIOPSTimeRemainingUnlimited {
            return true
        } else {
            return false
        }
    }
}
func PowerSourceChanged(context: UnsafeMutableRawPointer?) {
    let opaque = Unmanaged<PowerSourceMonitor>.fromOpaque(context!)
    let _self = opaque.takeUnretainedValue()
    DispatchQueue.main.async {
        _self.powerSourceChanged.toggle()
    }
}

struct NotchView_Previews: PreviewProvider {
    static var previews: some View {
        NotchView()
    }
}

I have some "property initializers run before 'self' is available" when i put startTimer() function inside Battery() class... But i think we're doing approximately the same things here... (Just stoled some var from your message on the way...)

What is UUID for?? What's the necessity to make it unique? var id: UUID()

2      

@Zenki  

Sorry... but why do i have to make calculations like max value and drain rate since i don't need them? I just want it to display the percentage. Which is done by getLevel(). Is this not good? not enough? or not the good way? It work as is. And we're far from my first question, so what's the link between them? I'm not a dev as i said, so please don't ask me so much, please... Tell me exactly what's wrong and what to change in my code. For me, asked changes are already made... Your post make me think there's still something wrong, with me, and with my code...

2      

@Zenki  

Thanks guys....

2      

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.