• iOS开发:搜索功能用上Liquid Glass是什么体验?
  • 发布于 1天前
  • 16 热度
    0 评论
前言
不知道大家有没有体验过今年 WWDC 2025 的 SwiftUI 新功能,苹果这次搞了个大动作,推出了 Liquid Glass 设计系统。说实话,我刚看到这个功能地时候,那种搜索体验真的把我震撼到了,特别是搜索框的动画效果,简直像液体一样流畅。之前写 App 的搜索界面,总觉得有点生硬,用户点一下搜索框,"啪"的一下就弹出来了,体验不是很好。但是 Liquid Glass 完全不一样,搜索的时候就像水滴融合一样自然,我第一次用的时候就想:这才是未来的搜索界面该有的样子啊!

今天就来跟大家聊聊这个 Liquid Glass 搜索功能到底怎么实现,其实代码不复杂,但效果真的很赞。

搜索功能用上 Liquid Glass 是什么体验?
我记得第一次用 iOS 原生搜索地时候,就觉得那个搜索框"啪"的一下弹出来,然后"唰"的一下收回去,虽然快,但总感觉少了点什么。Liquid Glass 就不一样了,你点搜索框的时候,它会像水滴一样慢慢"融化"变大,特别自然。

先来做个简单的搜索框
咱们先从最基础的搜索框开始,看看怎么给它加上 Liquid Glass 效果:
struct LiquidGlassSearchBar: View {
    @Stateprivatevar searchText = ""
    @Stateprivatevar isSearching = false
    @FocusStateprivatevar isFocused: Bool
    
    var body: some View {
        HStack {
            Image(systemName: "magnifyingglass")
                .foregroundColor(.secondary)
            
            TextField("搜索...", text: $searchText)
                .focused($isFocused)
                .onTapGesture {
                    withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
                        isSearching = true
                    }
                }
        }
        .padding(.horizontal, 15)
        .padding(.vertical, 12)
        .background(
            RoundedRectangle(cornerRadius: isSearching ? 25 : 15)
                .fill(.ultraThinMaterial)
                .glassEffect(.regular.tint(.blue.opacity(0.1)))
        )
        .scaleEffect(isSearching ? 1.05 : 1.0)
        .animation(.spring(response: 0.4, dampingFraction: 0.7), value: isSearching)
        .onChange(of: isFocused) { focused in
            withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
                isSearching = focused
            }
        }
    }
}

核心就是这个 .glassEffect(.regular.tint(.blue.opacity(0.1))),它会给搜索框加上玻璃质感。然后用 scaleEffect 让搜索框在点击时稍微放大一点,配合 spring 动画,就有了那种"水滴反弹"的感觉。

搜索结果也要有液体效果
搜索出来的结果,咱们也可以给它们加上 Liquid Glass 效果。关键是要用 GlassEffectContainer 把所有结果包起来:
struct SearchResult: Identifiable, Equatable {
    let id: UUID
    let title: String
    let subtitle: String
    let imageURL: URL
}

struct LiquidGlassSearchResultsView: View {
    @Stateprivatevar searchResults: [SearchResult] = []
    @Namespaceprivatevar namespace
    
    init(searchResults: [SearchResult] = []) {
        self._searchResults = State(initialValue: searchResults)
    }
    
    var body: some View {
        GlassEffectContainer(spacing: 8) {
            ForEach(searchResults) { result in
                SearchResultCard(result: result)
                    .glassEffect(.regular.tint(.white.opacity(0.1)).interactive())
                    .glassEffectID(result.id, in: namespace)
                    .transition(.asymmetric(
                        insertion: .move(edge: .trailing).combined(with: .opacity),
                        removal: .move(edge: .leading).combined(with: .opacity)
                    ))
            }
        }
        .animation(.spring(response: 0.6, dampingFraction: 0.8), value: searchResults)
    }
}

struct SearchResultCard: View {
    let result: SearchResult
    
    var body: some View {
        HStack {
            AsyncImage(url: result.imageURL) { image in
                image
                    .resizable()
                    .aspectRatio(contentMode: .fill)
            } placeholder: {
                RoundedRectangle(cornerRadius: 8)
                    .fill(.secondary.opacity(0.3))
            }
            .frame(width: 50, height: 50)
            .clipped()
            .cornerRadius(8)
            
            VStack(alignment: .leading, spacing: 4) {
                Text(result.title)
                    .font(.headline)
                
                Text(result.subtitle)
                    .font(.subheadline)
                    .foregroundColor(.secondary)
                    .lineLimit(2)
            }
            Spacer()
        }
        .padding(.horizontal, 16)
        .padding(.vertical, 12)
    }
}

这里的重点是 GlassEffectContainer,它能让多个搜索结果的玻璃效果融合在一起,看起来就像是一个整体。每个结果有独立的 glassEffectID,这样增删的时候动画会很流畅。

搜索建议也不能落下
用户输入的时候,通常会有搜索建议,这个我们也可以加上 Liquid Glass 效果:
struct LiquidGlassSearchSuggestions: View {
    @Stateprivatevar suggestions: [String]
    @Stateprivatevar selectedSuggestion: String?
    
    init(suggestions: [String], selectedSuggestion: String? = nil) {
        self.suggestions = suggestions
        self.selectedSuggestion = selectedSuggestion
    }
    
    var body: some View {
        LazyVStack(spacing: 0) {
            ForEach(suggestions, id: \.self) { suggestion in
                Button(action: {
                    withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
                        selectedSuggestion = suggestion
                    }
                }) {
                    HStack {
                        Image(systemName: "magnifyingglass")
                            .foregroundColor(.secondary)
                        
                        Text(suggestion)
                            .font(.body)
                        
                        Spacer()
                        
                        Image(systemName: "arrow.up.left")
                            .foregroundColor(.secondary)
                            .font(.caption)
                    }
                    .padding(.horizontal, 16)
                    .padding(.vertical, 12)
                }
            }
        }
        .background(
            RoundedRectangle(cornerRadius: 16)
                .fill(.regularMaterial)
                .glassEffect(.regular.tint(.white.opacity(0.1)))
        )
    }
}

这里地玻璃效果比较轻,因为搜索建议主要是让用户看清楚内容,太重的效果反而会影响阅读。

搜索历史也来点液体效果
搜索历史记录删除的时候,如果能有液体流动的效果就更棒了:
struct HistoryItemView: View {
    let text: String
    init(text: String) {
        self.text = text
    }
    
    var body: some View {
        HStack {
            Image(systemName: "clock")
                .foregroundColor(.secondary)
            Text(text)
                .font(.body)
                .lineLimit(1)
            Spacer()
            Image(systemName: "xmark.circle")
                .foregroundColor(.red)
                .onTapGesture {
                    // Handle delete action
                    print("Delete history item: \(text)")
                }
        }
        .padding(.horizontal, 16)
        .padding(.vertical, 10)
    }
}
struct LiquidGlassSearchHistory: View {
    @Stateprivatevar searchHistory: [String] = [
        "SwiftUI 26",
        "Liquid Glass 26",
        "iOS 18",
        
    ]
    @Namespaceprivatevar historyNamespace
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack {
                Text("搜索历史")
                    .font(.headline)
                
                Spacer()
                
                Button("清空") {
                    withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
                        searchHistory.removeAll()
                    }
                }
                .foregroundColor(.blue)
            }
            
            GlassEffectContainer(spacing: 6) {
                ForEach(searchHistory, id: \.self) { historyItem in
                    HistoryItemView(text: historyItem)
                        .glassEffect(.regular.tint(.gray.opacity(0.1)).interactive())
                        .glassEffectID(historyItem, in: historyNamespace)
                }
            }
            .animation(.spring(response: 0.5, dampingFraction: 0.8), value: searchHistory)
        }
        .padding()
        .background(
            RoundedRectangle(cornerRadius: 20)
                .fill(.ultraThinMaterial)
                .glassEffect(.regular.tint(.white.opacity(0.05)))
        )
    }
}

关键是用 glassEffectTransition 让删除动画更自然,就像液体消散一样。

把所有东西组合起来
现在把前面的搜索框、搜索结果、搜索建议都组合到一个页面里:
struct LiquidGlassSearchView: View {
    @Stateprivatevar searchText = ""
    @Stateprivatevar searchResults: [SearchResult] = []
    @Stateprivatevar searchSuggestions: [String] = ["SwiftUI", "Liquid Glass", "Swift", "iOS", "macOS", "Xcode", "WWDC", "SwiftUI 26", "Liquid Glass 26"]
    @FocusStateprivatevar isSearchFieldFocused: Bool
    
    var body: some View {
        NavigationView {
            VStack {
                // 搜索栏
                searchBar
                    .padding()
                
                // 根据状态显示不同内容
                if isSearchFieldFocused && !searchSuggestions.isEmpty {
                    LiquidGlassSearchSuggestions(suggestions: searchSuggestions)
                        .padding(.horizontal, 16)
                } elseif !searchResults.isEmpty {
                    LiquidGlassSearchResultsView(searchResults: searchResults)
                        .padding(.horizontal, 16)
                        
                } else {
                    LiquidGlassSearchHistory()
                        .padding(.horizontal, 16)
                }
                
                Spacer()
            }
            .background(
                LinearGradient(
                    colors: [.blue.opacity(0.1), .purple.opacity(0.1)],
                    startPoint: .topLeading,
                    endPoint: .bottomTrailing
                )
            )
            .navigationTitle("搜索")
            .background(.yellow)
        }
    }
    
    privatevar searchBar: some View {
        HStack {
            Image(systemName: "magnifyingglass")
                .foregroundColor(.secondary)
            
            TextField("搜索内容...", text: $searchText)
                .focused($isSearchFieldFocused)
                .onSubmit {
                    performSearch()
                }
            
            if isSearchFieldFocused && !searchText.isEmpty {
                Button("取消") {
                    searchText = ""
                    isSearchFieldFocused = false
                }
                .foregroundColor(.blue)
            }
        }
        .padding()
        .background(
            RoundedRectangle(cornerRadius: isSearchFieldFocused ? 25 : 15)
                .fill(.ultraThinMaterial)
                .glassEffect(.regular.tint(.blue.opacity(0.1)))
        )
        .scaleEffect(isSearchFieldFocused ? 1.02 : 1.0)
        .animation(.spring(response: 0.4, dampingFraction: 0.7), value: isSearchFieldFocused)
    }
    
    privatefunc performSearch() {
        // 执行搜索逻辑
        isSearchFieldFocused = false
        // 模拟搜索结果
        searchResults = [
            SearchResult(id: UUID(), title: "SwiftUI 26", subtitle: "新特性和改进", imageURL: URL(string: "https://example.com/swiftui26.png")!),
            SearchResult(id: UUID(), title: "Liquid Glass 26", subtitle: "探索液态玻璃效果", imageURL: URL(string: "https://example.com/liquidglass26.png")!)
        ]
    }
}
主要就是把各个组件按需显示,重点是保持动画的流畅性。

几个需要注意的地方
用 Liquid Glass 的时候,有几个坑要注意:
1. 别滥用玻璃效果
玻璃效果很费性能,搜索结果多的时候最好用 GlassEffectContainer 包起来:
// 这样做比较好
GlassEffectContainer(spacing: 8) {
    ForEach(items) { item in
        ItemView(item: item)
            .glassEffect(.regular.tint(.white.opacity(0.1)))
    }
}
2. 动画要用 spring
普通的动画看起来会比较突兀,spring 动画更自然:
.animation(.spring(response: 0.5, dampingFraction: 0.8), value: isSearching)
3. 兼容性问题
Liquid Glass 只支持 iOS 26+,旧设备需要做兼容处理:
@available(iOS 26.0, *)
private var liquidGlassSearchBar: some View {
    // Liquid Glass 实现
}

private var fallbackSearchBar: some View {
    // 传统搜索框
}
适用场景
这种搜索效果比较适合:
.内容类 App(新闻、视频)
.电商 App 的商品搜索
.社交 App 的用户搜索

.笔记、文档类 App


总结
说实话,Liquid Glass 搜索确实挺好用的,代码也不复杂,主要就是几个关键点:
.glassEffect - 核心修饰符,给界面加玻璃质感
.GlassEffectContainer - 把多个元素包起来,性能更好

.spring 动画 - 让过渡更自然,像液体一样流畅


我觉得这种搜索体验确实比传统地要好很多,特别是那种流动的感觉,用起来很舒服。如果你的 App 也有搜索功能,不妨试试这个效果,我这里嫌麻烦没有录屏,所以大家可以自己跑起来试试。其实苹果推出这种设计,我觉得是想让 iOS 的界面更自然,更贴近真实世界的物理效果。作为开发者,咱们也要跟上这个趋势,让自己的 App 体验更好一些。

你们有试过 Liquid Glass 吗?效果怎么样?
用户评论