Agora SwiftUI OpenLive Integration issues - swift

I'm trying to implement Agora to my existing SwiftUI app, I used this tutorial to implement it https://github.com/AgoraIO/Basic-Video-Call/tree/master/One-to-One-Video/Agora-iOS-Tutorial-SwiftUI-1to1
My Intention is to have a OpenLive video calls where I have only one Broadcaster and many audience to view it (similar to Instagram Live). unfortunately, I only found documentation on how to make a 1-1 video call and I was not able to let Audience join the channel and preview the live video stream.
This is my code:
import SwiftUI
import AgoraRtcKit
var isCurrentStreamer = true
struct AgoraView : View {
#EnvironmentObject var userDefaultData: UserDefaultDetails
#State var isLocalInSession = true
#State var isLocalAudioMuted = false
#State var isRemoteInSession = true
#State var isRemoteVideoMuted = false
let localCanvas = VideoCanvas()
let remoteCanvas = VideoCanvas()
private let videoEngine = VideoEngine()
private var rtcEngine: AgoraRtcEngineKit {
get {
return videoEngine.agoraEngine
}
}
var body: some View {
ZStack() {
VideoSessionView(
backColor: Color("c2"),
backImage: Image("big_logo"),
hideCanvas: false,
canvas: isCurrentStreamer ? localCanvas:remoteCanvas
).edgesIgnoringSafeArea(.all)
}.onAppear {
self.agoraLive(role: self.userDefaultData.AgoraRole, channelID: self.userDefaultData.currentAuctionId)
}
}
}
fileprivate extension AgoraView {
func agoraLive(role: AgoraClientRole, channelID: String){
// init AgoraRtcEngineKit
videoEngine.AgoraView = self
rtcEngine.enableDualStreamMode(false)
rtcEngine.setVideoEncoderConfiguration(
AgoraVideoEncoderConfiguration(
size: AgoraVideoDimension640x360,
frameRate: .fps15,
bitrate: AgoraVideoBitrateStandard,
orientationMode: .adaptative
)
)
if role == .broadcaster {
rtcEngine.enableVideo()
addLocalSession()
rtcEngine.startPreview()
}else{
addLocalSession()
rtcEngine.startPreview()
}
rtcEngine.joinChannel(byToken: Token, channelId: channelID, info: nil, uid: 0, joinSuccess: nil)
// Step 6, set speaker audio route
rtcEngine.setEnableSpeakerphone(true)
}
func addLocalSession() {
let videoCanvas = AgoraRtcVideoCanvas()
if self.userDefaultData.AgoraRole == .broadcaster {
videoCanvas.view = localCanvas.rendererView
}else {
videoCanvas.view = remoteCanvas.rendererView
}
videoCanvas.renderMode = .hidden
if isCurrentStreamer {
rtcEngine.setupLocalVideo(videoCanvas)
}else{
rtcEngine.setupRemoteVideo(videoCanvas)
}
}
}
Thanks.

It looks like you aren't hooking up the remote video view correctly. In order to see a remote user's video, you need to create a AgoraRtcVideoCanvas with that remote user's UID, and then pass it into setupRemoteVideo. What you're doing is trying to set up a remote view before you've even joined the call, and have no idea who is in it.
You need to implement the remoteVideoStateChangedOfUid callback on the AgoraRtcEngineDelegate in order to get the remote user's UID and hook it up to your canvas correctly.
Something like this:
extension AgoraView: AgoraRtcEngineDelegate {
func rtcEngine(_ engine: AgoraRtcEngineKit, remoteVideoStateChangedOfUid uid: UInt, state: AgoraVideoRemoteState, reason: AgoraVideoRemoteStateReason, elapsed: Int) {
if state == .starting {
let videoCanvas = AgoraRtcVideoCanvas()
videoCanvas.view = remoteCanvas.rendererView
videoCanvas.uid = uid
videoCanvas.renderMode = .hidden
rtcEngine.setupRemoteVideo(videoCanvas)
}
}
}
If you did the above as written, you'd have to set the AgoraView as the rtcEngine's delegate when you initialize it, as well.
If you're using the demo app provided, VideoEngine is currently set up as the delegate, and is doing this in firstRemoteVideoDecodedOfUid (which is now deprecated). It's likely that this code is not correctly hooked up to your SwiftUI views.

Related

SwiftUI: calling objectWillChange.send() not updating child view

I have a rather complicated set of views nested in views. When I trigger a button action, I pass along an optional block through my viewModel class which calls objectWillChange.send() on that viewModel and I know that it's being triggered because the other parts of my view are updating. One of the child views (which is observing that viewModel changes) doesn't update until I click on part of it (which changes viewModel.selectedIndex and triggers redraw so I know it's listening for published changes).
Why isn't the update triggering the child view (in this case PurchaseItemGrid) to redraw itself?
Here's where I setup the call to update...
struct RightSideView: View {
#ObservedObject var viewModel: TrenchesPurchases
var body: some View {
VStack {
...
PurchaseItemGrid(viewModel: viewModel) // <-- View not updating
Button {
viewModel.purchaseAction() {
viewModel.objectWillChange.send() // <-- Triggers redraw, reaches breakpoint here
}
} label: {
...
}
...
}
}
}
Here's where the optional is called (and I've not only visually confirmed this is happening as other parts of the view redraw, it also hits breakpoint here)...
class TrenchesPurchases: ObservableObject, CanPushCurrency {
// MARK: - Properties
#Published private var model = Purchases()
// MARK: - Properties: Computed
var selectedIndex: Int {
get { return model.selectedIndex }
set { model.selectedIndex = newValue }
}
var purchaseAction: BlockWithBlock {
{ complete in
...
complete?()
}
}
...
}
And here's the view that's not updating as expected...
struct PurchaseItemGrid: View {
#ObservedObject var viewModel: TrenchesPurchases
var body: some View {
VStack {
itemRow(indices: 0...3)
...
}
...
}
#ViewBuilder
func itemRow(indices range: ClosedRange<Int>) -> some View {
HStack {
ForEach(viewModel.purchaseItems[range], id: \.id) { item in
PurchaseItemView(item: item,
borderColor: viewModel.selectedIndex == item.id ? .green : Color(Colors.oliveGreen))
.onTapGesture { viewModel.selectedIndex = item.id }
}
}
}
}
Here's the code workingdog asked for...
struct Purchases {
// MARK: - Properties
var selectedIndex = 15
let items: [PurchaseItem] = buildCollectionOfItems()
// MARK: - Functions
// MARK: - Functions: Static
// TODO: Define Comments
static func buildCollectionOfItems() -> [PurchaseItem] {
return row0() + row1() + row2() + row3()
}
static func row0() -> [PurchaseItem] {
var items = [PurchaseItem]()
let grenade = Ammo(ammo: .grenade)
items.append(grenade)
let bullets = Ammo(ammo: .bullets)
items.append(bullets)
let infiniteBullets = Unlock(mode: .defense)
items.append(infiniteBullets)
let unlimitedInfantry = Unlock(mode: .offense)
items.append(unlimitedInfantry)
return items
}
static func row1() -> [PurchaseItem] {
var items = [PurchaseItem]()
for unit in UnitType.allCases {
let item = Unit(unit: unit)
items.append(item)
}
return items
}
static func row2() -> [PurchaseItem] {
var items = [PurchaseItem]()
let brits = NationItem(nation: .brits)
items.append(brits)
let turks = NationItem(nation: .turks)
items.append(turks)
let usa = NationItem(nation: .usa)
items.append(usa)
let insane = DifficultyItem(difficulty: .insane)
items.append(insane)
return items
}
static func row3() -> [PurchaseItem] {
var items = [PurchaseItem]()
let offenseLootBox = Random(mode: .offense)
items.append(offenseLootBox)
let defenseLootBox = Random(mode: .defense)
items.append(defenseLootBox)
let currency = Currency(isCheckin: false)
items.append(currency)
let checkIn = Currency(isCheckin: true)
items.append(checkIn)
return items
}
}
The issue I had was that the PurchaseItemGrid was noticing the observed item being published, but the change I was trying to trigger was in the PurchaseItemView which did not have an observed object.
I assumed that when the PurchaseItemGrid observed the change and was redrawn, the itemRow method would redraw a new collection of PurchaseItemView's that would then have their image updated to match the new state.
This was further compounded because the onTapGesture was triggering a redraw of the PurchaseItemView, and to be honest I'm still not sure how the PurchaseItemGrid could redraw itself while still using the same PurchaseItemView's in it's body; but it may have to do with how #ViewBuilder works and because the views were created in an entirely separate method.
So, long story short: make sure each view you want to update has some form of observer, don't rely on the parent's redraw to create new child views.

PDFKit's scaleFactorForSizeToFit isn't working to set zoom in SwiftUI (UIViewRepresentable)

I'm working on an app that displays a PDF using PDFKit, and I need to be able to set the minimum zoom level - otherwise the user can just zoom out forever. I've tried to set minScaleFactor and maxScaleFactor, and because these turn off autoScales, I need to set the scaleFactor to pdfView.scaleFactorForSizeToFit. However, this setting doesn't result in the same beginning zoom as autoScales and despite changing the actual scaleFactor number, the beginning zoom doesn't change. This photo is with autoScales on:
[![image with autoscales on][1]][1]
and then what happens when I use the scaleFactorForSizeToFit:
[![image with scaleFactorForSizeToFit][2]][2]
To quote the apple documentation for scaleFactorForSizeToFit, this is the
"size to fit" scale factor that autoScales would use for scaling the current document and layout.
I've pasted my code below. Thank you for your help.
import PDFKit
import SwiftUI
import Combine
class DataLoader : ObservableObject {
#Published var data : Data?
var cancellable : AnyCancellable?
func loadUrl(url: URL) {
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.receive(on: RunLoop.main)
.sink(receiveCompletion: { (completion) in
switch completion {
case .failure(let failureType):
print(failureType)
//handle potential errors here
case .finished:
break
}
}, receiveValue: { (data) in
self.data = data
})
}
}
struct PDFSwiftUIView : View {
#StateObject private var dataLoader = DataLoader()
var StringToBeLoaded: String
var body: some View {
VStack {
if let data = dataLoader.data {
PDFRepresentedView(data: data)
.navigationBarHidden(false)
} else {
CustomProgressView()
//.navigationBarHidden(true)
}
}.onAppear {
dataLoader.loadUrl(url: URL(string: StringToBeLoaded)!)
}
}
}
struct PDFRepresentedView: UIViewRepresentable {
typealias UIViewType = PDFView
let data: Data
let singlePage: Bool = false
func makeUIView(context _: UIViewRepresentableContext<PDFRepresentedView>) -> UIViewType {
let pdfView = PDFView()
// pdfView.autoScales = true
// pdfView.maxScaleFactor = 0.1
pdfView.minScaleFactor = 1
pdfView.scaleFactor = pdfView.scaleFactorForSizeToFit
pdfView.maxScaleFactor = 10
if singlePage {
pdfView.displayMode = .singlePage
}
return pdfView
}
func updateUIView(_ pdfView: UIViewType, context: UIViewRepresentableContext<PDFRepresentedView>) {
pdfView.document = PDFDocument(data: data)
}
func canZoomIn() -> Bool {
return false
}
}
struct ContentV_Previews: PreviewProvider {
static var previews: some View {
PDFSwiftUIView(StringToBeLoaded: "EXAMPLE_STRING")
.previewInterfaceOrientation(.portrait)
}
}
maybe it is to do with the sequence. This seems to work for me:
pdfView.scaleFactor = pdfView.scaleFactorForSizeToFit
pdfView.maxScaleFactor = 10.0
pdfView.minScaleFactor = 1.0
pdfView.autoScales = true
I was eventually able to solve this. The following code is how I managed to solve it:
if let document = PDFDocument(data: data) {
pdfView.displayDirection = .vertical
pdfView.autoScales = true
pdfView.document = document
pdfView.setNeedsLayout()
pdfView.layoutIfNeeded()
pdfView.minScaleFactor = UIScreen.main.bounds.height * 0.00075
pdfView.maxScaleFactor = 5.0
}
For some reason, the pdfView.scaleFactorForSizeToFit doesn't work - it always returns 0. This might be an iOS 15 issue - I noticed in another answer that someone else had the same issue. In the code above, what I did was I just scaled the PDF to fit the screen by screen height. This allowed me to more or less "autoscale" on my own. The code above autoscales the PDF correctly and prevents the user from zooming out too far.
This last solution works but you are kind of hardcoding the scale factor. so it only works if the page height is always the same. And bear in mind that macOS does not have a UIScreen and even under iPadOS there can be several windows and the window can have a different height than the screen height with the new StageManager... this worked for me:
first, wrap your swiftUIView (in the above example PDFRepresentedView) in a geometry reader. pass the view height (proxy.size.height) into the PDFRepresentedView.
in makeUIView, set
pdfView.maxScaleFactor = some value > 1
pdfView.autoScales = true
in updateUiView, set:
let pdfView = uiView
if let pageHeight = pdfView.currentPage?.bounds(for: .mediaBox).height {
let scaleFactor:CGFloat = self.viewHeight / pageHeight
pdfView.minScaleFactor = scaleFactor
}
Since my app also support macOS, I have written an NSViewRepresentable in the same way.
Happy coding!

SwiftUI - Audio keeps playing when changing views

I have a problem that's cropped up since I've updated my Xcode... Unless the user hits pause in the audio view. My audio player continues to play when the user changes views however I would like the audio to stop when the user exits the player view (ItemDetail) (for example when the user goes to Content View)
Previously I was using the below at the start of the Content View and that had worked but now it doesn't:
init() { sounds.pauseSounds() }
I've also tried this (which hasn't worked either):
}// end of navigation view .onAppear{sounds.pauseSounds()}
This is my sounds class:
class Sounds:ObservableObject {
var player:AVAudioPlayer?
// let shared = Sounds()
func playSounds(soundfile: String) {
if let path = Bundle.main.path(forResource: soundfile, ofType: nil){
do{
player = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
player?.prepareToPlay()
player?.play()
}catch {
print("Error can't find sound file or something's not working with the sounds model")
}
}
}
// This method used to work at pausing the sound (pre-update)
func pauseSounds() {
player?.pause()
}
// over rides the sound defaulting to bluetooth speaker only and sends it to phone speaker if bluetooth is not available (headset/speaker) however if it is available it'll play through that.
func overRideAudio() {
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(AVAudioSession.Category.playback, mode: .spokenAudio, options: .defaultToSpeaker)
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
} catch {
print("error.")
}
}
}
And this is the player view:
struct ItemDetail: View {
#State var isPlaying = false
var item : SessionsItem
#ObservedObject var soundsPlayer = Sounds()
var body: some View {
HStack {
Button(action: {
if self.isPlaying{
// Sounds.player?.pause()
self.isPlaying = false
soundsPlayer.pauseSounds()
}
else{
self.isPlaying = true
soundsPlayer.playSounds(soundfile: "\(self.item.name).mp3")
soundsPlayer.overRideAudio()
}
You are creating another Sound instance so you do not have access to the open audiio instance. You can make Sounds class as single instance or pass the Sounds instance to the detail. Dont create a new one.
onAppear is not a reliable way to do work like this SwiftUI does a lot of preloading (It is very apparent with a List). It depends on the device and its capabilities. Sometimes screens "appear" when they are preloaded not necessarily when the user is looking at it.
With the List example below you can see it in simulator.
On iPhone 8 I get 12 onAppear print statements in the console when I initially load it and on iPhone 12 Pro Max I get 17 print statements.
If the cells are more/less complex you get more/less statements accordingly. You should find another way to do this.
import SwiftUI
struct ListCellAppear: View {
var idx: Int
var body: some View {
HStack{
Text(idx.description)
Image(systemName: "checkmark")
}.onAppear(){
print(idx.description)
}
}
}
struct ListAppear: View {
var body: some View {
List(0..<1000){ idx in
ListCellAppear(idx: idx).frame(height: 300)
}
}
}
struct ListAppear_Previews: PreviewProvider {
static var previews: some View {
ListAppear()
}
}

Sharing Screenshot of SwiftUI view causes crash

I am grabbing a screenshot of a sub-view in my SwiftUI View to immediately pass to a share sheet in order to share the image.
The view is of a set of questions from a text array rendered as a stack of cards. I am trying to get a screenshot of the question and make it share-able along with a link to the app (testing with a link to angry birds).
I have been able to capture the screenshot using basically Asperi's answer to the below question:
How do I render a SwiftUI View that is not at the root hierarchy as a UIImage?
My share sheet launches, and I've been able to use the "Copy" feature to copy the image, so I know it's actually getting a screenshot, but whenever I click "Message" to send it to someone, or if I just leave the share sheet open, the app crashes.
The message says it's a memory issue, but doesn't give much description of the problem. Is there a good way to troubleshoot this sort of thing? I assume it must be something with how the screenshot is being saved in this case.
Here are my extensions of View and UIView to render the image:
extension UIView {
func asImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}
}
extension View {
func asImage() -> UIImage {
let controller = UIHostingController(rootView: self)
// locate far out of screen
controller.view.frame = CGRect(x: 0, y: CGFloat(Int.max), width: 1, height: 1)
UIApplication.shared.windows.first!.rootViewController?.view.addSubview(controller.view)
let size = controller.sizeThatFits(in: UIScreen.main.bounds.size)
controller.view.bounds = CGRect(origin: .zero, size: size)
controller.view.sizeToFit()
controller.view.backgroundColor = .clear
let image = controller.view.asImage()
controller.view.removeFromSuperview()
return image
}
}
Here's an abbreviated version of my view - the button is about halfway down, and should call the private function at the bottom that renders the image from the View/UIView extensions, and sets the "questionScreenShot" variable to the rendered image, which is then presented in the share sheet.
struct TopicPage: View {
var currentTopic: Topic
#State private var currentQuestions: [String]
#State private var showShareSheet = false
#State var questionScreenShot: UIImage? = nil
var body: some View {
GeometryReader { geometry in
Button(action: {
self.questionScreenShot = render()
if self.questionScreenShot != nil {
self.showShareSheet = true
} else {
print("Did not set screenshot")
}
}) {
Text("Share Question").bold()
}
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [questionScreenShot!])
}
}
}
private func render() -> UIImage {
QuestionBox(currentQuestion: self.currentQuestions[0]).asImage()
}
}
I've found a solution that seems to be working here. I start the variable where the questionScreenShot gets stored as nil to start:
#State var questionScreenShot: UIImage? = nil
Then I just make sure to set it to 'render' when the view appears, which means it loads the UIImage so if the user clicks "Share Question" it will be ready to be loaded (I think there was an issue earlier where the UIImage wasn't getting loaded in time once the sharing was done).
It also sets that variable back to nil on disappear.
.onAppear {
self.currentQuestions = currentTopic.questions.shuffled()
self.featuredQuestion = currentQuestions.last!
self.questionScreenShot = render()
}
.onDisappear {
self.questionScreenShot = nil
self.featuredQuestion = nil
}

How to pass an instance as a property using other properties in swift

I'm a newbie in swift and I'm trying to pass an instance of a class (CP) I made as a property of the ContentView struct for swift UI. This instance takes in parameter another property of the same ContentView struct. I want to be able to select an option from the alert menu, set the property to a variable that will determinate specific time sequences from a chronometer, and since the parameter of this instance is an inout string, the values will update in the instance. I found somebody in a similar situation on this website, and somebody suggested overriding the viewDidLoad function. However this does not apply to the SwiftUI framework.
Here is a sample of the code I believe is enough to understand the issue. débatCP, will be used in other elements so declaring it inside the first button action will not work.
struct ContentView: View {
#State var a = "Chronometre"
#State var partie = "Partie du débat"
#State var tempsString = "Temps ici"
#State var enCours = "CanadienParlementaire - Commencer"
#State var pausePlay = "pause"
#State var tempsMillieu = 420;
#State var tempsFermeture = 180
#State var round = 7;
#State var répartitionPM = "7/3"
var debatCP = CP(modePM: &répartitionPM)
#State private var showingAlert = false
//il va falloir un bouton pause
var body: some View {
VStack {
Text(String(self.round))
Text(a)
.font(.largeTitle)
.fontWeight(.semibold)
.lineLimit(nil)
Text(partie)
Button(action: {
self.showingAlert = true
if self.enCours != "Recommencer"{
let chrono = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { (chrono) in
self.debatCP.verifierEtatDebut(round: &self.round, tempsActuel: &self.tempsMillieu, pause: &self.pausePlay, partie: &self.partie, tempsStr: &self.tempsString)
if self.round <= 2{
self.debatCP.verifierEtatFin(round: &self.round, tempsActuel: &self.tempsFermeture, pause: &self.pausePlay, partie: &self.partie, tempsStr: &self.tempsString)
}
if self.round <= 0 {
chrono.invalidate()
self.enCours = "Canadien Parlementaire - Commencer"
}
}
}
else{
self.tempsMillieu = 420
self.tempsFermeture = 180
self.round = 7
}
self.enCours = "Recommencer"
self.a = "CP"
}, label: {
Text(enCours)
.alert(isPresented: $showingAlert) {
Alert(title: Text("6/4 ou 7/3"), message: Text("6/4 pour avoir plus de temps à la fin et 7/3 pour en avoir plus au début"), primaryButton: .default(Text("6/4"), action: {
self.répartitionPM = "6/4";
}), secondaryButton: .default(Text("7/3"), action: {
self.répartitionPM = "7/3"
}))
}
})
I get this error message : Cannot use instance member 'répartitionPM' within property initializer; property initializers run before 'self' is available
Edit
Here's the CP class
class CP:Debat{
init(modePM: inout String){
}
override var rondeFermeture:Int{
return 2;
}
override var tempsTotalMillieu : Int{
return 420;
};
override var tempsProtege:Int{//60
return 60;
}
override var tempsLibre:Int{//360
return 360;
}
override var tempsFermeture: Int{
return 180;
}
Along with a sample of the Defat Class
class Debat{
var rondeFermeture:Int{
return 0;
}
var tempsTotalMillieu:Int{//420
return 0;
};
var tempsProtege:Int{//60
return 0;
}
var tempsLibre:Int{//360
return 0;
}
var tempsFermeture:Int{
return 0
}
func formatTime(time:Int) -> String {
let minutes:Int = time/60
let seconds:Int = time%60
return String(minutes) + ":" + String(seconds)
}
func prochainTour(time: inout Int, round : inout Int)->Void{
if round > rondeFermeture {
let tempsAvantLaFin = time % self.tempsTotalMillieu
time -= tempsAvantLaFin
}
if round <= rondeFermeture{
let tempsAvantLaFin = time % self.tempsFermeture
time -= tempsAvantLaFin
}
}
#State var répartitionPM = "7/3"
var debatCP = CP(modePM: &répartitionPM)
The SwiftUI has different pattern to work with #State variables. It is not clear what is CP but anyway the following is the way to go
struct AContentView: View {
#State var repartitionPM = "7/3" // should be defined
var debatCP:CP!=nil // declare-only
init() {
debatCP = CP(modePM: $repartitionPM) // pass as binding
}
...
and CP should be something like
class CP:Debat{
#Binding var modePM:String
init(modePM:Binding <String>){
self._modePM = modePM
super.init()
}