Skip to content
This repository has been archived by the owner on Aug 25, 2023. It is now read-only.

Commit

Permalink
(QualityIssues) Add ConferenceView and ConferenceParticipantView - ne…
Browse files Browse the repository at this point in the history
…w layout for in-call video stream appearance
  • Loading branch information
VladimirBrejcha committed Jun 1, 2021
1 parent 7241e01 commit 09d6b7f
Show file tree
Hide file tree
Showing 6 changed files with 337 additions and 3 deletions.
6 changes: 3 additions & 3 deletions QualityIssues/QIAssets.xcassets/Contents.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
}
}
}
24 changes: 24 additions & 0 deletions QualityIssues/QIAssets.xcassets/videoOff.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "videoOff.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
75 changes: 75 additions & 0 deletions QualityIssues/UI/ConferenceParticipantView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2011-2020, Zingaya, Inc. All rights reserved.
*/

import UIKit

final class ConferenceParticipantView: UIView {

@IBOutlet private var labelContainer: UIView!
@IBOutlet private var nameLabel: UILabel!
@IBOutlet private var backgroundImage: UIImageView!
@IBOutlet private(set) var streamView: UIView!

private let cornerRadius: CGFloat = 4

var isVideoEnabled: Bool = false {
didSet { streamView.isHidden = !isVideoEnabled }
}

var isNameEnabled: Bool = false {
didSet {
if let name = name, !name.isEmpty {
nameLabel.isHidden = !isNameEnabled
labelContainer.isHidden = !isNameEnabled
}
}
}

var name: String? {
get { nameLabel.text }
set {
if let newValue = newValue, !newValue.isEmpty, isNameEnabled {
labelContainer.isHidden = false
} else {
labelContainer.isHidden = true
}
nameLabel.text = newValue
}
}

override init(frame: CGRect) {
super.init(frame: frame)
sharedInit()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
sharedInit()
}

private func sharedInit() {
setupFromNib()
isUserInteractionEnabled = true
contentMode = .scaleAspectFit
streamView.clipsToBounds = true
backgroundImage.layer.cornerRadius = cornerRadius
labelContainer.layer.cornerRadius = cornerRadius
streamView.layer.cornerRadius = cornerRadius
}

private func setupFromNib() {
if let view = UINib(
nibName: String(describing: Self.self),
bundle: Bundle(for: Self.self)
).instantiate(
withOwner: self,
options: nil
).first as? UIView {
addSubview(view)
view.frame = bounds
} else {
fatalError("Error loading \(self) from nib")
}
}
}
74 changes: 74 additions & 0 deletions QualityIssues/UI/ConferenceParticipantView.xib
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="ConferenceParticipantView" customModule="Quality_Issues" customModuleProvider="target">
<connections>
<outlet property="backgroundImage" destination="ehu-hQ-HRC" id="5Bz-8I-u6y"/>
<outlet property="labelContainer" destination="btL-ne-mNm" id="Qma-FY-Ol3"/>
<outlet property="nameLabel" destination="tMh-g5-4jg" id="S5Y-BZ-GcP"/>
<outlet property="streamView" destination="lUs-RH-WTQ" id="NNT-8D-fVv"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="414" height="235"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="videoOff" translatesAutoresizingMaskIntoConstraints="NO" id="ehu-hQ-HRC">
<rect key="frame" x="2" y="2" width="410" height="231"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="tintColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</imageView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lUs-RH-WTQ">
<rect key="frame" x="2" y="2" width="410" height="231"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<view hidden="YES" alpha="0.59999999999999998" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="btL-ne-mNm">
<rect key="frame" x="4" y="4" width="8" height="22"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="200" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="tMh-g5-4jg">
<rect key="frame" x="4" y="4" width="0.0" height="14"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="12"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="0.13333333333333333" green="0.062745098039215685" blue="0.25098039215686274" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="height" constant="22" id="6wj-AJ-cDN"/>
<constraint firstAttribute="trailing" secondItem="tMh-g5-4jg" secondAttribute="trailing" constant="4" id="VBa-uK-am4"/>
<constraint firstAttribute="bottom" secondItem="tMh-g5-4jg" secondAttribute="bottom" constant="4" id="b0O-Zt-gsV"/>
<constraint firstItem="tMh-g5-4jg" firstAttribute="top" secondItem="btL-ne-mNm" secondAttribute="top" constant="4" id="kDk-WD-cfD"/>
<constraint firstItem="tMh-g5-4jg" firstAttribute="leading" secondItem="btL-ne-mNm" secondAttribute="leading" constant="4" id="v40-ah-4ul"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="ehu-hQ-HRC" secondAttribute="trailing" constant="2" id="2ZA-ba-DQg"/>
<constraint firstItem="ehu-hQ-HRC" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="2" id="873-6z-ShQ"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="btL-ne-mNm" secondAttribute="bottom" constant="4" id="IVu-yp-rM2"/>
<constraint firstItem="lUs-RH-WTQ" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="2" id="Qcg-hu-MoQ"/>
<constraint firstItem="ehu-hQ-HRC" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" constant="2" id="Sk0-85-XOR"/>
<constraint firstAttribute="bottom" secondItem="ehu-hQ-HRC" secondAttribute="bottom" constant="2" id="Zds-e7-M5O"/>
<constraint firstAttribute="bottom" secondItem="lUs-RH-WTQ" secondAttribute="bottom" constant="2" id="cZh-4Y-k7f"/>
<constraint firstItem="btL-ne-mNm" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" constant="4" id="cgJ-8G-qgX"/>
<constraint firstAttribute="trailing" secondItem="lUs-RH-WTQ" secondAttribute="trailing" constant="2" id="leS-Vh-xCr"/>
<constraint firstItem="lUs-RH-WTQ" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" constant="2" id="luq-Ev-sV4"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="btL-ne-mNm" secondAttribute="trailing" constant="4" id="vaa-II-Ve1"/>
<constraint firstItem="btL-ne-mNm" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="4" id="zv4-19-LCA"/>
</constraints>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="137.68115942028987" y="-68.638392857142847"/>
</view>
</objects>
<resources>
<image name="videoOff" width="32" height="32"/>
</resources>
</document>
161 changes: 161 additions & 0 deletions QualityIssues/UI/ConferenceView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Copyright (c) 2011-2020, Zingaya, Inc. All rights reserved.
*/

import UIKit

@objc class ConferenceView: UIView {
private struct Participant {
let place: Int
var view: ConferenceParticipantView
}

private struct PendingParticipant {
var view: ConferenceParticipantView
var renderer: ((_ view: UIView?) -> Void)? = nil
}

private var enlargedParticipantId: String?
private var participants = [String: Participant]()
private var pendingParticipants = [String: PendingParticipant]()
private let maxNumberOfViews = 4
private var defaultParticipantView: ConferenceParticipantView {
let view = ConferenceParticipantView()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(toggleParticipant))
view.addGestureRecognizer(tapGesture)
view.frame = CGRect(x: self.bounds.width / 2, y: self.bounds.height / 2, width: 0, height: 0)
return view
}

@objc var enlargeParticipantHandler: ((Bool) -> Void)?

@objc func addParticipant(id: String, name: String? = nil) {
guard participants[id] == nil, pendingParticipants[id] == nil else {
print("addParticipant: unexpected branch")
return
}
let participantView = defaultParticipantView
participantView.name = name
if participants.count < maxNumberOfViews {
addSubview(participantView)
let participant = Participant(place: participants.count, view: participantView)
participants[id] = participant
} else {
let participant = PendingParticipant(view: participantView)
pendingParticipants[id] = participant
}
rearrange()
}

@objc func removeParticipant(id: String) {
if let participant = participants.removeValue(forKey: id) {
participant.view.removeFromSuperview()
if (id == enlargedParticipantId) {
enlargedParticipantId = nil
}
if participants.count < maxNumberOfViews,
let (participantId, pendingParticipant) = pendingParticipants.first {
pendingParticipants[participantId] = nil
participants[participantId] = Participant(
place: participants.count,
view: pendingParticipant.view
)
pendingParticipant.renderer?(pendingParticipant.view.streamView)
addSubview(pendingParticipant.view)
}
rearrange()
} else {
pendingParticipants[id] = nil
}
}

@objc func requestRendering(participantId id: String, render: @escaping (_ view: UIView?) -> Void) {
if let participant = participants[id] {
participant.view.isVideoEnabled = true
render(participant.view.streamView)
} else if pendingParticipants[id] != nil {
pendingParticipants[id]?.renderer = render
} else {
print("requestRendering: unexpected branch")
}
}

@objc func stopRendering(participantId id: String) {
if let participant = participants[id] {
participant.view.isVideoEnabled = false
} else if let pendingParticipant = pendingParticipants[id] {
pendingParticipant.view.isVideoEnabled = false
pendingParticipants[id]?.renderer = nil
}
}

@objc func updateParticipantName(id: String, name: String) {
if let participantView = participants[id]?.view {
participantView.name = name
} else if let pendingParticipantView = pendingParticipants[id]?.view {
pendingParticipantView.name = name
}
}

@objc private func toggleParticipant(_ sender: UIGestureRecognizer!) {
if let participantView = sender.view as? ConferenceParticipantView,
let id = participants.first(where: { _, value in value.view == participantView })?.key {
if id == enlargedParticipantId {
enlargedParticipantId = nil
} else {
enlargedParticipantId = id
}
}
rearrange()
}

override func layoutSubviews() {
rearrange()
super.layoutSubviews()
}

private func rearrange() {
DispatchQueue.main.async { () -> Void in
self.enlargeParticipantHandler?(self.enlargedParticipantId != nil)

let surface = self.bounds.size

if let rootParticipant = self.enlargedParticipantId {
guard let rootParticipantView = self.participants[rootParticipant]?.view
else { return }
self.participants.values.forEach { $0.view.alpha = 0 }
rootParticipantView.isNameEnabled = true
rootParticipantView.frame = CGRect(x: 0, y: 0, width: surface.width, height: surface.height)
rootParticipantView.alpha = 1

} else {
var w, h: CGFloat

switch self.participants.count {
case 0..<2:
w = 1; h = 1
case 2:
w = 2; h = 1
case 3..<5:
w = 2; h = 2
default:
return
}

let size = CGSize(width: surface.width / w, height: surface.height / h)
self.participants.values.forEach { participant in
let view = participant.view
view.alpha = 1
view.isNameEnabled = false
let x = participant.place % Int(w)
let y = participant.place / Int(w)
view.frame = CGRect(
origin: CGPoint(x: CGFloat(x) * size.width, y: CGFloat(y) * size.height),
size: size
)
view.layoutSubviews()
}
}
}
}
}

0 comments on commit 09d6b7f

Please sign in to comment.