Guillaume
Guillaume

Reputation: 1720

How to display data from Observable Object with high performances in swiftUI?

In my compass application, I display heading but the performances are not high: when I turn device quickly, all degrees are not displayed. I search the same performances that the natif compass application by Apple. For example, when the angle pass 359 to 0 degree, I make a vibration to notif user. But sometime the vibration not appear.

My LocationProvider class:

import SwiftUI
import CoreLocation
import Combine

public class LocationProvider: NSObject, CLLocationManagerDelegate, ObservableObject {

  private let locationManager: CLLocationManager
  public let heading = PassthroughSubject<CGFloat, Never>()

  @Published var currentHeading: CGFloat {
    willSet {
      heading.send(newValue)
    }
  }

  public override init() {
    currentHeading = 0
    locationManager = CLLocationManager()
    super.init()
    locationManager.delegate = self
    locationManager.desiredAccuracy = kCLLocationAccuracyBest
    locationManager.startUpdatingHeading()
    locationManager.requestWhenInUseAuthorization()
  }

  public func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
    self.currentHeading = CGFloat(newHeading.trueHeading)
  }
}

My contentView:

import SwiftUI
import CoreLocation

struct ContentView: View {

  @ObservedObject var location: LocationProvider = LocationProvider()

  @State var angle: CGFloat = 0

  var body: some View {
    VStack {
      Text(String(Double(-self.location.currentHeading + 360).stringWithoutZeroFraction) + "°")
        .font(.system(size: 80))
    }
    .onReceive(self.location.heading) { heading in
      compassTapticFeedback(-heading + 360)
    }
  }
}

public extension Double {
  var stringWithoutZeroFraction: String {
    return String(format: "%.0f", self)
  }
}

// Taptic feednack
func tapticFeedback(_ type: String) {
  switch type {
    case "heavy":
      let tapticFeedback = UIImpactFeedbackGenerator(style: .heavy)
      tapticFeedback.prepare()
      tapticFeedback.impactOccurred()
    case "medium":
      let tapticFeedback = UIImpactFeedbackGenerator(style: .medium)
      tapticFeedback.prepare()
      tapticFeedback.impactOccurred()
    case "light":
      let tapticFeedback = UIImpactFeedbackGenerator(style: .light)
      tapticFeedback.prepare()
      tapticFeedback.impactOccurred()
    default:
      let tapticFeedback = UIImpactFeedbackGenerator(style: .medium)
      tapticFeedback.prepare()
      tapticFeedback.impactOccurred()
  }
}

func compassTapticFeedback(_ angle: CGFloat) {
  switch Int(angle) {
    case 0:
      tapticFeedback("heavy")
    case 30:
      tapticFeedback("heavy")
    case 60:
      tapticFeedback("heavy")
    case 90:
      tapticFeedback("heavy")
    case 120:
      tapticFeedback("heavy")
    case 250:
      tapticFeedback("heavy")
    case 180:
      tapticFeedback("heavy")
    case 210:
      tapticFeedback("heavy")
    case 240:
      tapticFeedback("heavy")
    case 270:
      tapticFeedback("heavy")
    case 300:
      tapticFeedback("heavy")
    case 330:
      tapticFeedback("heavy")
    default:
      return
  }
}

May be it's because I do a lot of calculs on the angle to get a value without zero fraction? I don't know. Even if I don't sure that it the cause.

Upvotes: 0

Views: 195

Answers (1)

George
George

Reputation: 30421

The problem is that you are checking if the current angle is a multiple of 30. The issue with this is that if you rotate your device too fast, that number will be skipped and therefore not detected.

After doing the shortened version of compassTapticFeedback(_:) as shown on the second point in the Further tips below, I added some more code to make the method detect the skipped angles in between:

enum TapticSync {
    static var lastAngle: CGFloat = 0
}


func compassTapticFeedback(_ angle: CGFloat) {
    // If the angle is 30°, carry on. Otherwise do more checks
    if !Int(angle).isMultiple(of: 30) {
        // If there was an angle at a multiple of 30° skipped, carry on. Otherwise do not trigger haptic feedback.
        let minVal = min(angle, TapticSync.lastAngle)
        let maxVal = max(angle, TapticSync.lastAngle)
        let roundUpToNextMultiple = ceil(minVal / 30) * 30
        guard maxVal > roundUpToNextMultiple else { return }
    }

    // If the checks were passed, trigger the haptic feedback and update the last angle
    tapticFeedback("heavy")
    TapticSync.lastAngle = angle
}

Further tips

1: Haptic Feedback .prepare()

There is no point in tapticFeedback.prepare() in your case. As said in the documentation for .prepare():

Calling prepare() and then immediately triggering feedback (without any time in between) does not improve latency.

You should instead:

Think about when you can best prepare your generators. Call prepare() before the event that triggers feedback. The system needs time to prepare the Taptic Engine for minimal latency.

See the documentation for full info.

2: Checking angle

In your case, you do not need a switch to check every angle that is a multiple of 30. Instead, change compassTapticFeedback(_:) to look like this:

func compassTapticFeedback(_ angle: CGFloat) {
    if Int(angle).isMultiple(of: 30) {
        tapticFeedback("heavy")
    }
}

3: Your compass direction is reversed

The direction in degrees is reversed because of these parts (removed unnecessary self.):

-location.currentHeading + 360

compassTapticFeedback(-heading + 360)

Instead, so the degrees increase clockwise, use:

location.currentHeading

compassTapticFeedback(heading)

Upvotes: 1

Related Questions