design/halide
Halide: Profesjonalne funkcje w przystępnej formie
„Złożoność jest obecna — po prostu nie przytłacza użytkownika. Gdy oferuje się nieco bardziej przystępne wejście do tego świata, można rozbudzić entuzjazm bez wywoływania onieśmielenia."
Halide dowodzi, że profesjonalne narzędzia nie wymagają profesjonalnych interfejsów. Laureat Apple Design Award 2022 ukrywa funkcje na poziomie lustrzanek cyfrowych za intuicyjnymi gestami, odsłaniając możliwości dopiero wtedy, gdy użytkownik po nie sięga.
Dlaczego Halide ma znaczenie
Stworzona przez byłego projektanta Apple Sebastiaana de Witha i byłego inżyniera Twittera Bena Sandofsky'ego aplikacja Halide wypełnia lukę między prostym aparatem Apple a onieśmielającymi aplikacjami dla profesjonalistów.
Kluczowe osiągnięcia: - Apple Design Award 2022 - Uczyniła fotografię RAW przystępną dzięki funkcji Instant RAW - Spopularyzowała sterowanie aparatem za pomocą gestów - Obsługa jedną ręką na wszystkich rozmiarach iPhone'a - Ponad 10 000 recenzji z oceną 5 gwiazdek
Najważniejsze wnioski
- Ukrywaj złożoność, nie usuwaj jej — profesjonalne funkcje istnieją, ale pozostają niewidoczne do momentu wywołania; interfejs „symulatora lotu" nie jest jedynym sposobem udostępniania zaawansowanych możliwości
- Inteligentna aktywacja jest lepsza niż przyciski przełączające — narzędzia pojawiające się podczas interakcji (lupa ostrości podczas przeciągania) są bardziej intuicyjne niż ręczne sterowanie widocznością
- Gesty powinny być fizyczne w odczuciu — przeciągnięcie w pionie dla ekspozycji, w poziomie dla ostrości — bezpośrednio odwzorowuje zachowanie pokręteł prawdziwego aparatu
- Ucz podczas użytkowania — krótkie etykiety przy zmianach stanu pomagają użytkownikom naturalnie poznawać terminologię fotograficzną, bez osobnego samouczka
- Obsługa jedną ręką jako ograniczenie projektowe — projektowanie z myślą o zasięgu kciuka wymusza dobrą hierarchię informacji i priorytetyzację najważniejszych elementów sterowania
Podstawowa filozofia projektowania
„Nie przeszkadzaj"
Nadrzędna zasada Halide: narzędzia powinny pojawiać się, gdy są potrzebne, i znikać, gdy nie są.
INNE PROFESJONALNE APLIKACJE PODEJŚCIE HALIDE
───────────────────────────────────────────────────────────────────
Interfejs „symulatora lotu" Czysty wizjer
Wszystkie kontrolki widoczne zawsze Kontrolki pojawiają się na żądanie
Nauka UI przed użyciem Nauka podczas użytkowania
Stały układ Konfigurowalny pasek narzędzi
Tylko tryb manualny Tryb automatyczny + manualny dostępne
Kluczowa obserwacja: „Inne aplikacje do fotografii wyglądały jak symulatory lotu z mnóstwem pokręteł, co było onieśmielające, nawet dla kogoś takiego jak ja, kto uwielbia aparaty analogowe."
Inspiracja aparatami analogowymi
Halide przenosi dotykową przyjemność z aparatów analogowych na gesty na ekranie dotykowym.
APARAT ANALOGOWY ODPOWIEDNIK W HALIDE
───────────────────────────────────────────────────────────────────
Obrót pierścienia przysłony Przeciągnięcie w pionie dla ekspozycji
Obrót pierścienia ostrości Przeciągnięcie w poziomie dla ostrości
Mechaniczne rastry Wibracje haptyczne
Fizyczny wizjer Kompozycja pełnoekranowa
Wskaźnik światłomierza Cyfrowy histogram
Przełącznik Manual/Auto Przycisk przełączający AF
Cel projektowy: „Daj dziecku aparat, a zacznie bawić się pierścieniem przysłony, pokrętłami i przełącznikami. Może uda nam się przenieść odrobinę tej radości do aplikacji na kawałku szkła."
Biblioteka wzorców
1. Inteligentna aktywacja
Kontrolki pojawiają się kontekstowo, gdy są potrzebne, a następnie znikają. Nie wymaga ręcznego pokazywania/ukrywania.
Przykład: Lupa ostrości
STAN DOMYŚLNY
┌─────────────────────────────────────────────┐
│ │
│ [Wizjer] │
│ │
│ │
│ │
│ [Pokrętło ostrości na dole] │
└─────────────────────────────────────────────┘
GDY UŻYTKOWNIK DOTYKA POKRĘTŁA OSTROŚCI
┌─────────────────────────────────────────────┐
│ ┌──────────┐ │
│ │ LUPA │ ← Lupa ostrości pojawia się │
│ │ OSTROŚCI │ automatycznie │
│ │ (zoom) │ │
│ └──────────┘ │
│ │
│ [Pokrętło ostrości aktywne] │
└─────────────────────────────────────────────┘
GDY UŻYTKOWNIK PUSZCZA
┌─────────────────────────────────────────────┐
│ │
│ [Wizjer] │
│ ↑ │
│ Lupa zanika │
│ │
│ [Pokrętło ostrości w spoczynku] │
└─────────────────────────────────────────────┘
Koncepcja implementacji w SwiftUI:
struct IntelligentActivation<Content: View, Tool: View>: View {
@Binding var isInteracting: Bool
let content: Content
let tool: Tool
var body: some View {
ZStack {
content
tool
.opacity(isInteracting ? 1 : 0)
.animation(.easeInOut(duration: 0.2), value: isInteracting)
}
}
}
struct FocusControl: View {
@State private var isDragging = false
@State private var focusValue: Double = 0.5
var body: some View {
ZStack {
// Wizjer
CameraPreview()
// Lupa ostrości - pojawia się podczas przeciągania
IntelligentActivation(isInteracting: $isDragging, content: EmptyView()) {
FocusLoupeView(zoomLevel: 3.0)
.frame(width: 120, height: 120)
.position(x: 80, y: 80)
}
// Pokrętło ostrości
VStack {
Spacer()
FocusDial(value: $focusValue)
.gesture(
DragGesture()
.onChanged { _ in
isDragging = true
}
.onEnded { _ in
isDragging = false
}
)
}
}
}
}
2. Stopniowe ujawnianie
Proste domyślnie, złożone w razie potrzeby. Interfejs przekształca się w zależności od trybu.
Transformacja trybów:
TRYB AUTO (Domyślny)
┌─────────────────────────────────────────────┐
│ │
│ [Wizjer] │
│ │
│ │
│ ┌───┐ ┌───┐ │
│ │ [F] │ │ AF │ │
│ └───┘ └───┘ │
│ ┌───────────┐ │
│ │ (●) │ ← Migawka │
│ └───────────┘ │
└─────────────────────────────────────────────┘
Minimalne kontrolki. Po prostu fotografuj.
TRYB MANUALNY (Po dotknięciu „AF" → „MF")
┌─────────────────────────────────────────────┐
│ [Histogram] [Focus Peak] │
│ │
│ [Wizjer] │
│ │
│ ┌───┐ ┌───┐ ┌───┐ ┌────┐ │
│ │ [F] │ │WB │ │ISO│ │ MF │ │
│ └───┘ └───┘ └───┘ └────┘ │
│ ┌───────────┐ │
│ [────────────────●───────] ← Pokrętło ostrości │
│ │ (●) │ │
│ └───────────┘ │
└─────────────────────────────────────────────┘
Pełne kontrolki ujawnione. Profesjonalne narzędzia dostępne.
Implementacja:
enum CameraMode {
case auto
case manual
var visibleControls: [CameraControl] {
switch self {
case .auto:
return [.flash, .shutter, .autoFocus]
case .manual:
return [.flash, .whiteBalance, .iso, .shutter,
.manualFocus, .histogram, .focusPeaking]
}
}
}
struct AdaptiveToolbar: View {
@Binding var mode: CameraMode
var body: some View {
HStack {
ForEach(mode.visibleControls, id: \.self) { control in
ControlButton(control: control)
.transition(.asymmetric(
insertion: .scale.combined(with: .opacity),
removal: .scale.combined(with: .opacity)
))
}
}
.animation(.spring(response: 0.3), value: mode)
}
}
3. Sterowanie oparte na gestach
Ekspozycja i ostrość kontrolowane poprzez przeciąganie, jak fizyczne pokrętła aparatu.
KONTROLA EKSPOZYCJI (Przeciągnięcie w pionie w dowolnym miejscu)
↑ Przeciągnij w górę = jaśniej
│
───────┼─────── Aktualna ekspozycja
│
↓ Przeciągnij w dół = ciemniej
KONTROLA OSTROŚCI (Przeciągnięcie w poziomie na pokrętle)
Blisko ←────────●────────→ Daleko
│
Aktualna ostrość
SZYBKIE AKCJE (Przeciągnięcia od krawędzi)
← Lewa krawędź: Przełącz do biblioteki zdjęć
→ Prawa krawędź: Otwórz panel kontrolek manualnych
Implementacja:
struct GestureCamera: View {
@State private var exposure: Double = 0
@State private var focus: Double = 0.5
var body: some View {
ZStack {
CameraPreview()
// Gest ekspozycji (w dowolnym miejscu ekranu)
Color.clear
.contentShape(Rectangle())
.gesture(
DragGesture()
.onChanged { value in
// Przesunięcie w pionie mapowane na ekspozycję
let delta = -value.translation.height / 500
exposure = max(-2, min(2, exposure + delta))
HapticsEngine.impact(.light)
}
)
// Wizualna informacja zwrotna
VStack {
Spacer()
ExposureIndicator(value: exposure)
.opacity(exposure != 0 ? 1 : 0)
}
}
}
}
struct ExposureIndicator: View {
let value: Double
var body: some View {
HStack {
Image(systemName: value > 0 ? "sun.max.fill" : "moon.fill")
Text(String(format: "%+.1f", value))
.monospacedDigit()
}
.font(.caption)
.padding(8)
.background(.ultraThinMaterial)
.clipShape(Capsule())
}
}
4. Edukacyjne mikroteksty
Gdy użytkownicy wchodzą w interakcję z kontrolkami, krótkie etykiety uczą ich terminologii.
UŻYTKOWNIK DOTYKA „AF" → Przełącza na „MF"
┌────────────────────────────────┐
│ Ostrość manualna │ ← Pojawia się na chwilę
│ Przeciągnij, aby dostosować odległość │
└────────────────────────────────┘
Znika po 2 sekundach
UŻYTKOWNIK WŁĄCZA FOCUS PEAKING
┌────────────────────────────────┐
│ Focus Peaking │
│ Czerwone podświetlenie = ostrość │
└────────────────────────────────┘
Implementacja:
struct EducationalToast: View {
let title: String
let description: String
@Binding var isVisible: Bool
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.headline)
Text(description)
.font(.caption)
.foregroundStyle(.secondary)
}
.padding()
.background(.ultraThinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
.opacity(isVisible ? 1 : 0)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation { isVisible = false }
}
}
}
}
// Użycie
struct FocusModeToggle: View {
@State private var showToast = false
@Binding var isManual: Bool
var body: some View {
Button(isManual ? "MF" : "AF") {
isManual.toggle()
showToast = true
}
.overlay(alignment: .top) {
EducationalToast(
title: isManual ? "Ostrość manualna" : "Autofokus",
description: isManual
? "Przeciągnij, aby dostosować odległość"
: "Dotknij, aby ustawić ostrość na obiekcie",
isVisible: $showToast
)
.offset(y: -60)
}
}
}
5. Profesjonalne narzędzia (Histogram, Focus Peaking, Zebry)
Profesjonalne narzędzia wizualizacji dostępne, ale nieprzytłaczające.
Opcje histogramu:
OPCJE UMIEJSCOWIENIA
┌─────────────────────────────────────────────┐
│ [▁▂▃▅▇█▅▃▁] │ ← Górny róg (minimalny)
│ │
│ [Wizjer] │
│ │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ [Wizjer] │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ ▁▂▃▅▇██▅▃▂▁ RGB │ │ ← Dół (szczegółowy)
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
TYPY HISTOGRAMU (przełączane dotknięciem)
1. Luminancja (monochromatyczny)
2. RGB (kanały kolorów)
3. Waveform (rozkład pionowy)
Nakładka focus peaking:
struct FocusPeakingOverlay: View {
let enabled: Bool
let peakingColor: Color = .red
var body: some View {
if enabled {
// W wersji produkcyjnej użyto by shadera Metal
// Podświetla krawędzie w płaszczyźnie ostrości
GeometryReader { _ in
// Nakładka podświetlająca obszary w ostrości
// Na podstawie detekcji krawędzi przy aktualnej odległości ostrzenia
}
.blendMode(.plusLighter)
}
}
}
struct ZebraOverlay: View {
let threshold: Double // 0.0 do 1.0
let pattern: ZebraPattern = .diagonal
enum ZebraPattern {
case diagonal
case horizontal
}
var body: some View {
// Nakładka w paski na prześwietlonych obszarach
// Pomaga zidentyfikować przycinanie przed zrobieniem zdjęcia
}
}
6. Konfigurowalny pasek narzędzi
Użytkownicy mogą zmieniać układ narzędzi, aby dopasować go do swojego przepływu pracy.
DOMYŚLNY PASEK NARZĘDZI
[Lampa] [Siatka] [Timer] ─(●)─ [RAW] [AF] [Ustawienia]
PRZYTRZYMAJ, ABY DOSTOSOWAĆ
┌─────────────────────────────────────────────┐
│ Przeciągnij, aby zmienić kolejność │
│ │
│ [Lampa] [Siatka] [Timer] │
│ ↕ ↕ ↕ │
│ [RAW] [Makro] [Ustawienia] │
│ │
│ Ukryte: │
│ [Zebra] [Waveform] [Głębia] │
│ │
│ [Przywróć domyślne] [Gotowe] │
└─────────────────────────────────────────────┘
Implementacja:
struct CustomizableToolbar: View {
@State private var tools: [CameraTool] = CameraTool.defaults
@State private var isEditing = false
var body: some View {
HStack {
ForEach(tools) { tool in
ToolButton(tool: tool)
.draggable(tool) // iOS 16+
}
}
.onLongPressGesture {
isEditing = true
}
.sheet(isPresented: $isEditing) {
ToolbarEditor(tools: $tools)
}
}
}
struct ToolbarEditor: View {
@Binding var tools: [CameraTool]
var body: some View {
NavigationStack {
List {
Section("Aktywne narzędzia") {
ForEach($tools) { $tool in
ToolRow(tool: tool)
}
.onMove { from, to in
tools.move(fromOffsets: from, toOffset: to)
}
}
Section("Dostępne narzędzia") {
ForEach(CameraTool.hidden(from: tools)) { tool in
ToolRow(tool: tool)
.onTapGesture {
tools.append(tool)
}
}
}
}
.navigationTitle("Dostosuj pasek narzędzi")
.toolbar {
Button("Gotowe") { /* zamknij */ }
}
}
}
}
System projektowania wizualnego
Paleta kolorów
extension Color {
// Halide używa minimalnej palety kolorów dla elementów UI
static let halideBlack = Color(hex: "#000000")
static let halideWhite = Color(hex: "#FFFFFF")
static let halideGray = Color(hex: "#8E8E93")
// Akcent dla stanów aktywnych
static let halideYellow = Color(hex: "#FFD60A") // Aktywne wskaźniki
// Focus peaking
static let halideFocusPeak = Color(hex: "#FF3B30") // Czerwona nakładka
static let halideZebra = Color(hex: "#FFFFFF").opacity(0.5)
}
Typografia
struct HalideTypography {
// Etykiety UI (małe, kapitaliki)
static let controlLabel = Font.system(size: 10, weight: .bold, design: .rounded)
.smallCaps()
// Wartości (monospace dla liczb)
static let valueDisplay = Font.system(size: 14, weight: .medium, design: .monospaced)
// Podpowiedzi edukacyjne
static let tooltipTitle = Font.system(size: 14, weight: .semibold)
static let tooltipBody = Font.system(size: 12, weight: .regular)
}
Animacje i wibracje haptyczne
System informacji zwrotnej haptycznej
struct HapticsEngine {
static func impact(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
let generator = UIImpactFeedbackGenerator(style: style)
generator.impactOccurred()
}
static func exposureChange() {
// Lekka wibracja przy zmianie ekspozycji
impact(.light)
}
static func focusLock() {
// Średnia wibracja przy zablokowaniu ostrości
impact(.medium)
}
static func shutterPress() {
// Mocna wibracja dla migawki
impact(.heavy)
}
static func modeSwitch() {
// Informacja zwrotna wyboru przy zmianie trybu
let generator = UISelectionFeedbackGenerator()
generator.selectionChanged()
}
}
Animacje kontrolek
// Animacja obrotu pokrętła
struct FocusDial: View {
@Binding var value: Double
var body: some View {
GeometryReader { geo in
// Wizualizacja pokrętła ze znacznikami
Circle()
.stroke(Color.white.opacity(0.3), lineWidth: 2)
.overlay {
// Znaczniki obracają się wraz z wartością
ForEach(0..<12) { i in
Rectangle()
.fill(Color.white)
.frame(width: 2, height: 10)
.offset(y: -geo.size.height / 2 + 15)
.rotationEffect(.degrees(Double(i) * 30))
}
}
.rotationEffect(.degrees(value * 360))
.animation(.spring(response: 0.2, dampingFraction: 0.8), value: value)
}
}
}
Wnioski dla naszej pracy
1. Ukrywaj złożoność, nie usuwaj jej
Profesjonalne funkcje istnieją, ale pozostają poza zasięgiem wzroku, dopóki nie zostaną wywołane.
2. Inteligentna aktywacja > Przyciski przełączające
Narzędzia pojawiające się podczas interakcji (lupa ostrości podczas przeciągania) wygrywają z ręcznym pokazywaniem/ukrywaniem.
3. Gesty powinny być fizyczne w odczuciu
Przeciągnięcie w pionie dla ekspozycji, w poziomie dla ostrości — odwzorowuje prawdziwe pokrętła aparatu.
4. Ucz podczas użytkowania
Krótkie etykiety przy zmianach stanu pomagają użytkownikom naturalnie poznawać terminologię.
5. Obsługa jedną ręką to ograniczenie projektowe
Projektowanie z myślą o zasięgu kciuka wymusza dobrą hierarchię informacji.
Najczęściej zadawane pytania
Czym Halide różni się od innych profesjonalnych aplikacji aparatu?
Większość profesjonalnych aplikacji aparatu wyświetla wszystkie kontrolki jednocześnie, tworząc to, co twórcy Halide nazywają interfejsami „symulatorów lotu". Halide zaczyna od czystego wizjera i ujawnia kontrolki tylko wtedy, gdy są potrzebne. Tryb automatyczny pokazuje minimalne UI; przełączenie na tryb manualny stopniowo odsłania histogram, focus peaking, ISO i kontrolki balansu bieli. To podejście sprawia, że funkcje na poziomie lustrzanek są dostępne bez przytłaczania nowych użytkowników.
Czym jest inteligentna aktywacja i dlaczego ma znaczenie?
Inteligentna aktywacja oznacza, że narzędzia pojawiają się automatycznie, gdy wchodzisz w interakcję z powiązanymi kontrolkami, a następnie znikają, gdy przestajesz. Na przykład lupa ostrości pojawia się, gdy dotkniesz pokrętła ostrości, i zanika, gdy je puścisz. Eliminuje to potrzebę przycisków przełączających do pokazywania/ukrywania widoków pomocniczych, redukując wizualny bałagan i obciążenie poznawcze, jednocześnie zapewniając, że narzędzia są zawsze dostępne, gdy są potrzebne.
Jak działają gesty sterowania w Halide?
Halide mapuje gesty na ekranie dotykowym na fizyczne kontrolki aparatu. Przeciągnięcie w pionie w dowolnym miejscu ekranu dostosowuje ekspozycję (jak pierścień przysłony), przeciągnięcie w poziomie na pokrętle ostrości dostosowuje odległość ostrzenia (jak pierścień ostrości). Te gesty obejmują informację zwrotną haptyczną, która naśladuje mechaniczne rastry prawdziwych pokręteł aparatu. Przeciągnięcia od krawędzi zapewniają szybki dostęp do biblioteki zdjęć i panelu kontrolek manualnych.
Jakie profesjonalne narzędzia wizualizacji oferuje Halide?
Halide oferuje histogram (tryby luminancji, RGB lub waveform), focus peaking (czerwone podświetlenie na ostrych krawędziach) i zebry (ukośne paski na prześwietlonych obszarach). Te narzędzia pojawiają się jako nakładki na wizjerze i mogą być umieszczone w różnych lokalizacjach. Każde narzędzie jest opcjonalne i dostępne z konfigurowalnego paska narzędzi, który użytkownicy mogą przestawiać, aby dopasować do swojego przepływu pracy.
Dlaczego Halide używa metafor z aparatów analogowych w swoim interfejsie?
Projektanci Halide zaobserwowali, że dzieci instynktownie bawią się pierścieniami przysłony, pokrętłami i przełącznikami aparatów analogowych. Ta dotykowa radość jest nieobecna w aplikacjach na ekrany dotykowe. Tłumacząc mechaniczne kontrolki aparatu na gesty przeciągania z informacją zwrotną haptyczną, Halide odtwarza część tej fizyczności. Podejście to wykorzystuje również dekady ewolucji UX aparatów, używając znanych modeli mentalnych zamiast wymyślania nowych paradygmatów.