在苹果的SwiftUI教程“‘Scrumdinger”中,ScrumTimer模型发布了三个属性,而speakers属性不包括在内。
class ScrumTimer: ObservableObject {
/// The name of the meeting attendee who is speaking.
@Published var activeSpeaker = ""
/// The number of seconds since the beginning of the meeting.
@Published var secondsElapsed = 0
/// The number of seconds until all attendees have had a turn to speak.
@Published var secondsRemaining = 0
/// NOTE: This property is not published.
private(set) var speakers: [Speaker] = []
}但是,当我们单击MeetingFooterView中的“Next”按钮,它将更新scrumTimer.speakers数组和其他已发布的scrumTimer属性时,MeetingFooterView将被重新绘制。
ScrumTimer:
/*
See LICENSE folder for this sample’s licensing information.
*/
import Foundation
/// Keeps time for a daily scrum meeting. Keep track of the total meeting time, the time for each speaker, and the name of the current speaker.
class ScrumTimer: ObservableObject {
/// A struct to keep track of meeting attendees during a meeting.
struct Speaker: Identifiable {
/// The attendee name.
let name: String
/// True if the attendee has completed their turn to speak.
var isCompleted: Bool
/// Id for Identifiable conformance.
let id = UUID()
}
/// The name of the meeting attendee who is speaking.
@Published var activeSpeaker = ""
/// The number of seconds since the beginning of the meeting.
@Published var secondsElapsed = 0
/// The number of seconds until all attendees have had a turn to speak.
@Published var secondsRemaining = 0
/// All meeting attendees, listed in the order they will speak.
private(set) var speakers: [Speaker] = []
/// The scrum meeting length.
private(set) var lengthInMinutes: Int
/// A closure that is executed when a new attendee begins speaking.
var speakerChangedAction: (() -> Void)?
private var timer: Timer?
private var timerStopped = false
private var frequency: TimeInterval { 1.0 / 60.0 }
private var lengthInSeconds: Int { lengthInMinutes * 60 }
private var secondsPerSpeaker: Int {
(lengthInMinutes * 60) / speakers.count
}
private var secondsElapsedForSpeaker: Int = 0
private var speakerIndex: Int = 0
private var speakerText: String {
return "Speaker \(speakerIndex + 1): " + speakers[speakerIndex].name
}
private var startDate: Date?
/**
Initialize a new timer. Initializing a time with no arguments creates a ScrumTimer with no attendees and zero length.
Use `startScrum()` to start the timer.
- Parameters:
- lengthInMinutes: The meeting length.
- attendees: A list of attendees for the meeting.
*/
init(lengthInMinutes: Int = 0, attendees: [DailyScrum.Attendee] = []) {
self.lengthInMinutes = lengthInMinutes
self.speakers = attendees.speakers
secondsRemaining = lengthInSeconds
activeSpeaker = speakerText
}
/// Start the timer.
func startScrum() {
changeToSpeaker(at: 0)
}
/// Stop the timer.
func stopScrum() {
timer?.invalidate()
timer = nil
timerStopped = true
}
/// Advance the timer to the next speaker.
func skipSpeaker() {
changeToSpeaker(at: speakerIndex + 1)
}
private func changeToSpeaker(at index: Int) {
if index > 0 {
let previousSpeakerIndex = index - 1
speakers[previousSpeakerIndex].isCompleted = true
}
secondsElapsedForSpeaker = 0
guard index < speakers.count else { return }
speakerIndex = index
activeSpeaker = speakerText
secondsElapsed = index * secondsPerSpeaker
secondsRemaining = lengthInSeconds - secondsElapsed
startDate = Date()
timer = Timer.scheduledTimer(withTimeInterval: frequency, repeats: true) { [weak self] timer in
if let self = self, let startDate = self.startDate {
let secondsElapsed = Date().timeIntervalSince1970 - startDate.timeIntervalSince1970
self.update(secondsElapsed: Int(secondsElapsed))
}
}
}
private func update(secondsElapsed: Int) {
secondsElapsedForSpeaker = secondsElapsed
self.secondsElapsed = secondsPerSpeaker * speakerIndex + secondsElapsedForSpeaker
guard secondsElapsed <= secondsPerSpeaker else {
return
}
secondsRemaining = max(lengthInSeconds - self.secondsElapsed, 0)
guard !timerStopped else { return }
if secondsElapsedForSpeaker >= secondsPerSpeaker {
changeToSpeaker(at: speakerIndex + 1)
speakerChangedAction?()
}
}
/**
Reset the timer with a new meeting length and new attendees.
- Parameters:
- lengthInMinutes: The meeting length.
- attendees: The name of each attendee.
*/
func reset(lengthInMinutes: Int, attendees: [DailyScrum.Attendee]) {
self.lengthInMinutes = lengthInMinutes
self.speakers = attendees.speakers
secondsRemaining = lengthInSeconds
activeSpeaker = speakerText
print("## reset speakers: \(speakers)")
print("## reset lengthInMinites: \(lengthInMinutes)")
print("## reset secondsRemaining: \(secondsRemaining)")
print("## reset activeSpeaker: \(activeSpeaker)")
}
}
extension DailyScrum {
/// A new `ScrumTimer` using the meeting length and attendees in the `DailyScrum`.
var timer: ScrumTimer {
ScrumTimer(lengthInMinutes: lengthInMinutes, attendees: attendees)
}
}
extension Array where Element == DailyScrum.Attendee {
var speakers: [ScrumTimer.Speaker] {
if isEmpty {
return [ScrumTimer.Speaker(name: "Speaker 1", isCompleted: false)]
} else {
return map { ScrumTimer.Speaker(name: $0.name, isCompleted: false) }
}
}
}MeetingFooterView:
struct MeetingFooterView: View {
let speakers: [ScrumTimer.Speaker]
var skipAction: ()->Void
private var speakerNumber: Int? {
guard let index = speakers.firstIndex(where: { !$0.isCompleted }) else { return nil}
return index + 1
}
private var isLastSpeaker: Bool {
print("### MeetingFooterView updated!")
print("### speakers is: \(speakers)")
return speakers.dropLast().allSatisfy { $0.isCompleted }
}
private var speakerText: String {
guard let speakerNumber = speakerNumber else { return "No more speakers" }
return "Speaker \(speakerNumber) of \(speakers.count)"
}
var body: some View {
if #available(iOS 15.0, *) {
Self._printChanges()
}
return VStack {
HStack {
if isLastSpeaker {
Text("Last Speaker")
} else {
Text(speakerText)
Spacer()
Button(action: skipAction) {
Image(systemName: "forward.fill")
}
.accessibilityLabel("Next speaker")
}
}
}
.padding([.bottom, .horizontal])
}
}MeetingView:
struct MeetingView: View {
@Binding var scrum: DailyScrum
@StateObject var scrumTimer = ScrumTimer()
private var player: AVPlayer { AVPlayer.sharedDingPlayer }
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 16)
.fill(scrum.theme.mainColor)
VStack {
MeetingHeaderView(secondsElapsed: scrumTimer.secondsElapsed, secondsRemaining: scrumTimer.secondsRemaining, theme: scrum.theme)
Circle()
.strokeBorder(lineWidth: 24)
MeetingFooterView(speakers: scrumTimer.speakers, skipAction: scrumTimer.skipSpeaker)
// DebugView(speakers: scrumTimer.speakers)
}
}
.padding()
.foregroundColor(scrum.theme.accentColor)
.onAppear {
print("### .onAppear() run")
scrumTimer.reset(lengthInMinutes: scrum.lengthInMinutes, attendees: scrum.attendees)
scrumTimer.speakerChangedAction = {
player.seek(to: .zero)
player.play()
}
scrumTimer.startScrum()
}
.onDisappear {
scrumTimer.stopScrum()
}
.navigationBarTitleDisplayMode(.inline)
}
}问题是:MeetingFooterView只依赖于scrumTimer.speakers,这是一个未发布的属性,它是如何根据scrumTimer.speakers是否已经改变而决定重新绘制的?我的意思是,scrumTimer.speakers是@StateObject模型的一个未发布的属性,如果它的值发生了变化,视图应该根据它重新绘制吗?
完整的代码可以获取这里。
谢谢。
=====更新=====
今天,当我关注同一个教程项目和下一章时,有一个关于ObservableObject的注释:“ObservableObject包括一个objectWillChange发布者,当它的一个@Published属性即将改变时,它就会发出。当scrums值发生变化时,任何观察ScrumStore实例的视图都会再次呈现出来。”既然@StateObject's rawValue 就是 an ObservableObject,那么它们的工作方式应该是一样的。
作为一个初学SwiftUI的人,我的困惑是@StateObject是在属性级别还是在对象级别作为一个整体发布其值。我首先认为@StateObject在属性级别发布其更改,因此发布的var secondsElapsed值更改不应导致MeetingFooterView依赖于未发布的var speakers属性重绘,即使speakers值已经更改。但这是不对的。正如上面提到的那样,“ObservableObject包括an(单一) objectWillChange publisher,当one的@Published属性即将改变时,它就会发出”,这意味着@StateObject将其更改作为一个整体对象传递。
有了这种理解,我才知道为什么speakers属性不必是@Published。当程序运行时,ScumTimer的secondsElapsed属性每秒钟更新一次,计时器也会更改speakers值if secondsElapsedForSpeaker >= secondsPerSpeaker,例如每100秒钟就会发生一次。因此,即使speakers属性没有发布,它的值变化也将每秒钟进行一次评估,因为secondsElapsed每秒钟都会发生变化,@StateObject会将其值更改作为一个整体进行传递。
但是,如果我们像下面这样修改它的属性,只发布speakers属性,
/// The name of the meeting attendee who is speaking.
var activeSpeaker = ""
/// The number of seconds since the beginning of the meeting.
var secondsElapsed = 0
/// The number of seconds until all attendees have had a turn to speak.
var secondsRemaining = 0
/// All meeting attendees, listed in the order they will speak.
@Published var speakers: [Speaker] = []事情不会成功的。secondsElapsed仍然每秒钟更新一次,但是由于它没有发布,scrumTimer不会每秒钟传递一次更改。只有当speaker的值发生变化时,例如当我们单击"Next“按钮时,它才会传递更改。
发布于 2022-08-10 09:54:09
MeetingFooterView的主体被调用两次的原因是扬声器数组发生了变化。这是因为ScrumTimer首先是不带attendees的,当attendees.speakers被称为isEmpty时,检查结果是设置一个初始的默认扬声器,然后在onAppear中设置实际的参与者,从而改变扬声器。此外,如果没有实际的参与者,那么就再次创建一个新的初始默认扬声器,因此即使在这种情况下,扬声器数组仍然被更改。
MeetingFooterView: @self changed.
Printing description of self.speakers:
▿ 1 element
▿ 0 : Speaker
- name : "Speaker 1"
- isCompleted : false
▿ id : 220F9930-E774-4A16-B62F-BF45C0100FDB
MeetingFooterView: @self changed.
Printing description of self.speakers:
▿ 1 element
▿ 0 : Speaker
- name : "John"
- isCompleted : false
▿ id : C53672E2-5043-4364-81E0-C9D44ACDF8CFprintChanges应该向self.speakers changed表明,不知道为什么没有。
您可以通过删除ScrumTimer init方法并将var lengthInMinutes默认为0来整理这一点。还请注意,DailyScum在扩展中有2个timer vars:var timer: Timer?和var timer: ScrumTimer。首先,移除扩展部分。然后在onDissapear中,当scrum停止时:
let newHistory = History(attendees: scrum.attendees, lengthInMinutes: scrum.timer.secondsElapsed / 60)这个对scrum.timer的调用创建了一个新的ScrumTimer对象!
应:
let newHistory = History(attendees: scrum.attendees, lengthInMinutes: scrumTimer.secondsElapsed / 60)这个示例可能有更多的问题,请小心使用它!
https://stackoverflow.com/questions/73280727
复制相似问题