Skip to content

Instantly share code, notes, and snippets.

@CodeSlicing
Created July 28, 2021 22:49
Show Gist options
  • Save CodeSlicing/d988f0aec23b35152131cac687e91c9a to your computer and use it in GitHub Desktop.
Save CodeSlicing/d988f0aec23b35152131cac687e91c9a to your computer and use it in GitHub Desktop.
Native AppleTvRemote source code for episode on multiple cursors in Xcode
//
// AppleTVRemoteNative.swift
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
// AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// Created by Adam Fordyce on 30/07/2021.
// Copyright © 2020 Adam Fordyce. All rights reserved.
//
import SwiftUI
private let silverGradient = LinearGradient(gradient: Gradient(colors: [Color(white: 0.85), Color(white: 0.65)]), startPoint: .topLeading, endPoint: .bottomTrailing)
private let outerDialGradient = LinearGradient(gradient: Gradient(colors: [Color(white: 0.18), Color(white: 0.02)]), startPoint: .topLeading, endPoint: .bottomTrailing)
private let buttonBackgroundColor = Color(white: 0.05)
private let buttonForegroundColor = Color(white: 0.65)
struct AppleTVRemoteNative: View {
var body: some View {
let siriButtonGradient = LinearGradient(gradient:
Gradient(stops: [
Gradient.Stop(color: Color(white: 0.9), location: 0),
Gradient.Stop(color: Color(white: 0.65), location: 0.15),
Gradient.Stop(color: Color(white: 0.65), location: 0.85),
Gradient.Stop(color: Color(white: 0.4), location: 1),
]), startPoint: .top, endPoint: .bottom)
GeometryReader { (geo: GeometryProxy) in
let controlSize = getControlSize(from: geo)
ZStack {
let shadowOffset = controlSize.width * 0.03
Color.black.mask(ControlBackground(controlSize: controlSize))
.offset(x: shadowOffset, y: shadowOffset)
.blur(radius: shadowOffset)
Rectangle()
.fill(siriButtonGradient)
.frame(width: controlSize.width * 0.03, height: controlSize.height * 0.1)
.offset(x: controlSize.width * 0.5, y: controlSize.height * -0.296)
ControlBackground(controlSize: controlSize)
VStack(spacing: 0) {
MicrophoneAndPowerButton(controlSize: controlSize)
Group {
let mainDialShadowOffset = controlSize.width * 0.005
MainDial(controlSize: controlSize)
.padding(.bottom, controlSize.height * 0.01)
.shadow(radius: controlSize.width * 0.01, x: mainDialShadowOffset, y: mainDialShadowOffset)
let clusterShadowOffset = controlSize.width * 0.007
ButtonCluster(controlSize: controlSize)
.shadow(radius: controlSize.width * 0.015, x: clusterShadowOffset, y: clusterShadowOffset)
Spacer()
}
.foregroundColor(buttonForegroundColor)
.font(.system(size: controlSize.width * 0.1))
}
}
.frame(width: controlSize.width, height: controlSize.height)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
private func getControlSize(from geo: GeometryProxy) -> CGSize {
let widthAsFractionOfHeight: CGFloat = 0.25
if geo.size.width > geo.size.height * widthAsFractionOfHeight {
return CGSize(width: geo.size.height * widthAsFractionOfHeight, height: geo.size.height)
} else {
return CGSize(width: geo.size.width, height: geo.size.width * (1 / widthAsFractionOfHeight))
}
}
}
private struct ControlBackground: View {
let controlSize: CGSize
var body: some View {
RoundedRectangle(cornerRadius: controlSize.width * 0.18)
.fill(silverGradient)
}
}
private struct MicrophoneAndPowerButton: View {
let controlSize: CGSize
var body: some View {
let silverGradient = LinearGradient(gradient: Gradient(colors: [Color(white: 0.85), Color(white: 0.55)]), startPoint: .topLeading, endPoint: .bottomTrailing)
let microphoneButtonSize = controlSize.width * 0.19
ZStack {
Capsule()
.frame(width: controlSize.width * 0.065, height: controlSize.height * 0.005)
HStack {
Spacer()
Circle()
.stroke(silverGradient, lineWidth: controlSize.width * 0.004)
.overlay(Image(systemName: "power").resizable()
.aspectRatio(contentMode: .fit)
.frame(width: controlSize.width * 0.085, height: controlSize.width * 0.085))
.frame(width: microphoneButtonSize, height: microphoneButtonSize)
}
}
.padding([.top, .horizontal], controlSize.width * 0.1)
.padding(.bottom, controlSize.width * 0.07)
}
}
private struct MainDial: View {
let controlSize: CGSize
var body: some View {
let innerDialGradient = RadialGradient(gradient: Gradient(colors: [Color(white: 0.14), Color(white: 0.03)]), center: UnitPoint(x: 0.8, y: 0.8), startRadius: controlSize.width * 0.1, endRadius: controlSize.width * 0.6)
let ringBlurRadius = max(controlSize.width * 0.004, 0.6)
let ringShadowOffset = max(controlSize.width * 0.004, 1)
let mainDialSize = controlSize.width * 0.9
let outerDialSize = controlSize.width * 0.73
let innerDialSize = controlSize.width * 0.53
let dotSize = controlSize.width * 0.018
ZStack {
Color.black
Group {
Circle()
.fill(outerDialGradient)
Circle()
.fill(outerDialGradient)
.rotationEffect(.degrees(180))
.frame(width: outerDialSize, height: outerDialSize)
}
.blur(radius: controlSize.width * 0.04)
Group {
innerDialGradient
.clipShape(Circle())
Circle()
.stroke(LinearGradient(gradient: Gradient(colors: [Color(white: 0.3), Color(white: 0.5)]), startPoint: .topLeading, endPoint: .bottomTrailing), lineWidth: controlSize.width * 0.005)
.offset(x: ringShadowOffset, y: ringShadowOffset)
.blur(radius: ringBlurRadius)
.blendMode(.screen)
Circle()
.stroke(Color(white: 0.03), lineWidth: controlSize.width * 0.01)
}
.frame(width: innerDialSize, height: innerDialSize)
ForEach(0..<4) { index in
Circle()
.fill(buttonForegroundColor)
.frame(width: dotSize, height: dotSize)
.offset(x: controlSize.width * 0.4)
.rotationEffect(.degrees(90 * Double(index)))
}
}
.clipShape(Circle())
.frame(width: mainDialSize, height: mainDialSize)
}
}
private struct ButtonCluster: View {
let controlSize: CGSize
var body: some View {
let buttonDiameter = controlSize.width * 0.35
let concaveButtonGradient = RadialGradient(gradient: Gradient(colors: [Color(white: 0.14), Color(white: 0.03)]), center: UnitPoint(x: 0.8, y: 0.8), startRadius: controlSize.width * 0.05, endRadius: controlSize.width * 0.28)
VStack(spacing: controlSize.height * 0.02) {
HStack(spacing: controlSize.width * 0.08) {
ZStack {
concaveButtonGradient
.frame(width: buttonDiameter, height: buttonDiameter)
.clipShape(Circle())
Image(systemName: "chevron.left")
}
SingleButton(diameter: buttonDiameter, sfSymbol: "tv")
}
HStack(spacing: controlSize.width * 0.08) {
VStack(spacing: controlSize.height * 0.02) {
SingleButton(diameter: buttonDiameter, sfSymbol: "playpause")
SingleButton(diameter: buttonDiameter, sfSymbol: "speaker.slash")
}
Capsule()
.fill(buttonBackgroundColor)
.frame(width: buttonDiameter, height: controlSize.height * 0.195)
.overlay(
VStack {
Image(systemName: "plus")
.padding(.vertical, controlSize.height * 0.03)
Spacer()
Image(systemName: "minus")
.padding(.vertical, controlSize.height * 0.04)
}
)
}
}
}
}
private struct SingleButton: View {
let diameter: CGFloat
let sfSymbol: String
var body: some View {
Circle()
.fill(Color(white: 0.05))
.overlay(Image(systemName: sfSymbol))
.frame(width: diameter, height: diameter)
}
}
struct AppleTVRemoteNative_Previews: PreviewProvider {
struct AppleTVRemoteNative_Harness: View {
var body: some View {
HStack(alignment: .top, spacing: 100) {
AppleTVRemoteNative()
.frame(width: 200, height: 800)
}
.padding(.vertical, 80)
.padding(.horizontal, 120)
.background(Color(white: 0.1))
}
}
static var previews: some View {
AppleTVRemoteNative_Harness()
.previewLayout(.sizeThatFits)
}
}
@davejacobsen
Copy link

Adam you are a legend. This is incredible and has inspired me to up my game. Cheers and looking forward to what you create in the future.

@CodeSlicing
Copy link
Author

Sorry I missed this comment. You’re welcome Dave! :o)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment