草庐IT

iOS widget 小组件开发

Hahn_z 2023-03-28 原文

iOS widget 小组件开发

Github地址

项目选择对应语言项目小组件部分 Github地址 https://github.com/HahnLoving/iOS_Study

iOS 多个widget调试问题

iOS 多个widget调试问题
https://www.jianshu.com/p/d0993b2c6e34

iOS widget 小组件 秒级刷新
https://www.jianshu.com/p/40d631f32260

创建项目

1.png
2.png

widget 代码说明

Provider

struct Provider: TimelineProvider {
    // 占位视图,例如网络请求失败、发生未知错误、第一次展示小组件都会展示这个view
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date())
    }

    // 定义Widget预览中如何展示,所以提供默认值要在这里
    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date())
        completion(entry)
    }

    // 决定 Widget 何时刷新
    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []

        // Generate a timeline consisting of five entries an hour apart, starting from the current date.
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate)
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

SimpleEntry

// 渲染 Widget 所需的数据模型,需要遵守TimelineEntry协议
struct SimpleEntry: TimelineEntry {
    let date: Date
}

MainWidgetEntryView

// 渲染的view
struct MainWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        Text(entry.date, style: .time)
    }
}

MainWidget

@main
struct MainWidget: Widget {
    // 主件唯一标识符
    let kind: String = "MainWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            MainWidgetEntryView(entry: entry)
        }
        // 标题
        .configurationDisplayName("My Widget")
        // 详情
        .description("This is an example widget.")
        // 枚举设置
        .supportedFamilies([.systemMedium, .systemSmall, .systemLarge])
    }
}

MainWidget_Previews

// SwiftUI Xcode 测试预览视图
struct MainWidget_Previews: PreviewProvider {
    static var previews: some View {
        MainWidgetEntryView(entry: SimpleEntry(date: Date()))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}

效果图

3.jpg

widget 分组

widget 有多组和单组区别。
单组包含 小,中,大,特大(iOS 15)
多组包含 小,小,小

单组的例子如系统的天气widget包含小,中,大三个组件

4.jpg
5.jpg
6.jpg

多组以支付宝为例子。包含两个小的组件

7.jpg
8.jpg

单组件适配开发

MainWidgetEntryView

// 渲染的view
struct MainWidgetEntryView : View {
    var entry: Provider.Entry
    
    // 判断小组件的类型
    @Environment(\.widgetFamily) var family: WidgetFamily

    var body: some View {
//        Text(entry.date, style: .time)
        switch family {
        case .systemSmall:
            VStack(alignment: .center, spacing: 0) {
                Text("小")
                Spacer().frame(height: 20)
                Text(entry.date, style: .time)
            }
        case .systemMedium:
            VStack(alignment: .center, spacing: 0) {
                Text("中")
                Spacer().frame(height: 20)
                Text(entry.date, style: .time)
            }
        default:
            VStack(alignment: .center, spacing: 0) {
                Text("大")
                Spacer().frame(height: 20)
                Text(entry.date, style: .time)
            }
        }
    }
}

效果图

9.jpg
10.jpg

多组件开发 WidgetBundle

多组件开发使用了 WidgetBundle
下面用了以下5个案例讲解多组件开发
1.网络请求 NetWorkData
2.网络图片 NetWorkImage
3.编辑小组件 Edit
4.和app本地数据进行交互和点击交互 AppData

MainWidget
先注释全部代码

import WidgetKit
import SwiftUI
import Intents

struct SimpleEntry: TimelineEntry {
    public let date: Date
}

struct PlaceholderView : View {
    //这里是PlaceholderView - 提醒用户选择部件功能
    var body: some View {
        Text("Place Holder")
    }
}

@main
struct MainWidget: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
//        OneWordWidget()
//        FristWidget(title: "hahn", desc: "hahn1")
//        CountDownWidget()
//        Demo(title: "Demo", desc: "Demo")
        NetWorkData(title: "网络请求小组件", desc: "网络请求小组件列表")
        NetWorkImage(title: "网络图片小组件", desc: "网络图片小组件列表")
        Edit(title: "编辑小组件", desc: "编辑小组件列表")
        AppData(title: "app数据交互小组件", desc: "app数据交互小组件列表")
    }
}

新建分组的widget

新建SwiftUI文件

11.png

新建 SirKit Intent Definition File 文件

12.png

记得勾选这两个,不然会无法编辑的

13.png

新建 Intent

14.png

Title就是你对应Widget 名字
Category 选择View
勾Widgets

15.png

网络请求 NetWorkData

先按照上面的步骤创建 NetWorkData 的widget 和 Intent文件

注意需要info.plist设置可以网络请求。如果主项目没有网络请求,先做一次网络请求。
我这里使用了Moya 框架进行Api 请求。

为widget 扩展添加pod

platform :ios, '14.0'
use_frameworks!
inhibit_all_warnings!

source 'https://github.com/CocoaPods/Specs.git'

target 'StudySwiftUI' do

# swift
end

# Extension 名字  在Targets里面可以查看名字
target 'MainWidgetExtension' do
  pod 'ObjectMapper'
  pod 'Moya'
  pod 'ObjectMapper'
  pod 'HandyJSON'
  pod 'SwiftyJSON'
end

请求的json 结构如下

{
    "items":[
        {
            "title":"Quiz for Designing",
            "subTitle":"834 questions in tottal",
            "icon":"tlIconDesign1",
            "correctRate":80,
            "submit":48,
            "starCount":4
        },
        {
            "title":"Quiz for Coding",
            "subTitle":"1000 questions in tottal",
            "icon":"tlIconMobile1",
            "correctRate":98,
            "submit":68,
            "starCount":5
        },
        {
            "title":"Quiz for Officing",
            "subTitle":"569 questions in tottal",
            "icon":"tlIconOffice1",
            "correctRate":60,
            "submit":43,
            "starCount":3
        },
        {
            "title":"Quiz for Painting",
            "subTitle":"321 questions in tottal",
            "icon":"tlIconDesign2",
            "correctRate":87,
            "submit":48,
            "starCount":5
        }
    ]
}

使用HandyJSON 创建Model

import Foundation
import HandyJSON

struct ZModel: HandyJSON {
    var items: [ZQuiz] = []
}

struct ZQuiz: HandyJSON {
    
    var title : String = ""
    var subTitle : String = ""
    var icon : String = ""
    var correctRate : Int = 0
    var submit : Int = 0
    var starCount : Int = 0
}

我们取json数组第一个字典的title渲染出来

NetWorkData.swift

import WidgetKit
import SwiftUI
import Intents

struct NetWorkDataProvider: IntentTimelineProvider {
    func placeholder(in context: Context) -> NetWorkDataEntry {
        NetWorkDataEntry(date: Date(), configuration: NetWorkDataIntent(), displaySize: context.displaySize, model: ZModel())
    }

    // 定义Widget预览中如何展示,所以提供默认值要在这里
    func getSnapshot(for configuration: NetWorkDataIntent, in context: Context, completion: @escaping (NetWorkDataEntry) -> ()) {
        let entry = NetWorkDataEntry(date: Date(), configuration: configuration, displaySize: context.displaySize, model: ZModel())
        completion(entry)
    }

    // 决定 Widget 何时刷新
    func getTimeline(for configuration: NetWorkDataIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        
        // API请求
        NetWorkRequest(API.getQuizListApi, modelType: ZModel.self) { (model, responseModel) in
            // 每隔2个小时刷新。
            let entry = NetWorkDataEntry(date: Date(), configuration: configuration, displaySize: context.displaySize, model: model)
                // refresh the data every two hours
            let expireDate = Calendar.current.date(byAdding: .hour, value: 2, to: Date()) ?? Date()
            let timeline = Timeline(entries: [entry], policy: .after(expireDate))

            completion(timeline)

        } failureCallback: { (responseModel) in

            
        }
        
    }
}

struct NetWorkDataEntry: TimelineEntry {
    let date: Date
    let configuration: NetWorkDataIntent
    let displaySize: CGSize
    let model: ZModel
}

struct NetWorkDataEntryView : View {
    var entry: NetWorkDataProvider.Entry

    var body: some View {
        Text(entry.model.items.first?.title ?? "0")
    }
}


// 单个
//@main
struct NetWorkData: Widget {
    let kind: String = "NetWorkData"
    var title: String = ""
    var desc: String = ""
    
    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: NetWorkDataIntent.self, provider: NetWorkDataProvider()) { entry in
            NetWorkDataEntryView(entry: entry)
        }
        .configurationDisplayName(title)
        .description(desc)
        .supportedFamilies([.systemMedium, .systemSmall])
//        .supportedFamilies([.systemMedium, .systemLarge, .systemSmall])
    }
}

效果
渲染出来第一个元素的title

16.png

网络图片 NetWorkImage

先按照上面的步骤创建 NetWorkImage 的widget 和 Intent文件
注意小组件目前是不支持异步下载的和动画的
所以图片是需要同步下载的

WidgetImageLoader 先对图片下载封装

import Foundation
import SwiftUI

enum WidgetError: Error {
    case netError //网络请求出错
    case dataError //数据解析错误
}


/*
 
 由于不支持异步加载图片
 所以暂时在网络请求好之后,直接下载好全部图片
 使用NSCache暂存图片
 */
class WidgetImageLoader {
    
    static var shareLoader = WidgetImageLoader()
    private var cache = NSCache<NSURL, UIImage>()
    
    /// 下载单张图片
    /// - Parameters:
    ///   - imageUrl: 图片URL
    ///   - completion: 成功的回调
    func downLoadImage(imageUrl: String?,completion: @escaping (Result<Image, WidgetError>) -> Void) {
        if let imageUrl = imageUrl {
            if let cacheImage  = self.cache.object(forKey: NSURL(string: imageUrl)!) {
                completion(.success(Image(uiImage: cacheImage)))
            } else {
                URLSession.shared.dataTask(with: URL(string: imageUrl)!) { (data, response, error) in
                    if let data = data,
                       let image = UIImage(data: data) {
                        self.cache.setObject(image, forKey: NSURL(string: imageUrl)!)
                        completion(.success(Image(uiImage: image)))
                    } else {
                        completion(.failure(WidgetError.netError))
                    }
                }.resume()
            }
        } else {
            completion(.failure(WidgetError.dataError))
        }
    }
    
    /// 批量下载图片
    /// - Parameters:
    ///   - imageAry: 图片数组集合
    ///   - placeHolder: 占位图,可传可不传
    ///   - completion: 成功回调
    func downLoadImage(imageAry:[String],placeHolder:Image?,completion: @escaping (Result<[Image], WidgetError>) -> Void) {
        let group:DispatchGroup = DispatchGroup()
        var array = [Image]()
        for image in imageAry {
            group.enter()
            self.downLoadImage(imageUrl: image) { result in
                let image : Image
                if case .success(let response) = result {
                    image = response
                } else {
                    image = placeHolder ?? Image("")
                }
                array.append(image)
                group.leave()
            }
        }
        group.notify(queue: DispatchQueue.main) {
            completion(.success(array))
        }
    }
    
    /// 获取image
    /// - Parameters:
    ///   - imageUrl: 图片地址
    ///   - placeHolderImage: 占位图,请尽量传入
    /// - Returns: 返回结果
    func getImage(_ imageUrl:String, _ placeHolderImage:UIImage?) -> UIImage {
        if let cacheImage  = self.cache.object(forKey: NSURL(string: imageUrl)!) {
            return cacheImage
        } else {
            if let cacheImag = placeHolderImage {
                return cacheImag
            } else {
                return UIImage()
            }
        }
    }
}

NetWorkImage

import WidgetKit
import SwiftUI
import Intents

struct NetWorkImageProvider: IntentTimelineProvider {
    
    var img = Image("widget_background_test")
    
    func placeholder(in context: Context) -> NetWorkImageEntry {
        NetWorkImageEntry(date: Date(), configuration: NetWorkImageIntent(), displaySize: context.displaySize, img: img)
    }

    // 定义Widget预览中如何展示,所以提供默认值要在这里
    func getSnapshot(for configuration: NetWorkImageIntent, in context: Context, completion: @escaping (NetWorkImageEntry) -> ()) {
        let entry = NetWorkImageEntry(date: Date(), configuration: configuration, displaySize: context.displaySize, img: img)
        completion(entry)
    }

    // 决定 Widget 何时刷新
    func getTimeline(for configuration: NetWorkImageIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        
        var img = Image("widget_background_test")
        // 占位图
        WidgetImageLoader.shareLoader.downLoadImage(imageUrl: "https://lmg.jj20.com/up/allimg/tx18/0217202027012.jpg") { result in
            switch result {
            case .success(let image):
                print("成功 = \(image)")
                img = image
                // 每隔2个小时刷新。
                let entry = NetWorkImageEntry(date: Date(), configuration: configuration, displaySize: context.displaySize,img: img)
                    // refresh the data every two hours
                let expireDate = Calendar.current.date(byAdding: .hour, value: 2, to: Date()) ?? Date()
                let timeline = Timeline(entries: [entry], policy: .after(expireDate))
                
                completion(timeline)
            case .failure(let error):
                print("失败 = \(error)")
            }
        }

    }
}

struct NetWorkImageEntry: TimelineEntry {
    let date: Date
    let configuration: NetWorkImageIntent
    let displaySize: CGSize
    let img: Image
}

struct NetWorkImageEntryView : View {
    var entry: NetWorkImageProvider.Entry

    var body: some View {
        entry.img
            .resizable()
            .frame(width: entry.displaySize.width, height: entry.displaySize.height, alignment: .center)
    }
}


// 单个
//@main
struct NetWorkImage: Widget {
    let kind: String = "NetWorkImage"
    var title: String = ""
    var desc: String = ""
    
    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: NetWorkImageIntent.self, provider: NetWorkImageProvider()) { entry in
            NetWorkImageEntryView(entry: entry)
        }
        .configurationDisplayName(title)
        .description(desc)
        .supportedFamilies([.systemMedium, .systemSmall])
//        .supportedFamilies([.systemMedium, .systemLarge, .systemSmall])
    }
}

效果

17.png

编辑小组件 Edit

先按照上面的步骤创建 Edit 的widget 和 Intent文件
这里需要注意要Intent文件一个勾上主项目和你widget,不然小组件编辑无法载入。

在Edit Intent文件创建title字段


18.png

Edit

import WidgetKit
import SwiftUI
import Intents


struct EditProvider: IntentTimelineProvider {
    func placeholder(in context: Context) -> EditEntry {
        EditEntry(date: Date(), configuration: EditIntent())
    }
    //    typealias Entry = EditEntry
    func getSnapshot(for configuration: EditIntent, in context: Context, completion: @escaping (EditEntry) -> Void) {
        let entry = EditEntry(date: Date(), configuration: configuration)
        completion(entry)
    }
    func getTimeline(for configuration: EditIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
        let entry = EditEntry(date: Date(), configuration: configuration)
            // refresh the data every two hours
        let expireDate = Calendar.current.date(byAdding: .hour, value: 2, to: Date()) ?? Date()
        let timeline = Timeline(entries: [entry], policy: .after(expireDate))
        completion(timeline)
    }
}

struct EditEntry: TimelineEntry {
    public let date: Date
    let configuration: EditIntent
}

struct EditEntryView : View {
    //这里是Widget的类型判断
    var entry: EditProvider.Entry
    
    @ViewBuilder
    var body: some View {
        VStack(alignment: .center) {
            if entry.configuration.title == "请编辑小组件" {
                Text("请编辑小组件吧")
                    .font(.headline)
                    .foregroundColor(Color.gray)
            }else {
                Text(entry.configuration.title ?? "")
                    .font(.headline)
                    .foregroundColor(Color.gray)
            }
        }
        .padding(.all)
    }
    
}



struct Edit: Widget {
    private let kind: String = "Edit"
    var title: String = ""
    var desc: String = ""
    
    public var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: EditIntent.self, provider: EditProvider()) { entry in
            EditEntryView(entry: entry)
        }
        .configurationDisplayName(title)
        .description(desc)
        .supportedFamilies([.systemSmall])
        
    }
}

效果


19.png
20.png
21.png

和app本地数据进行交互 AppData

先按照上面的步骤创建 AppData 的widget 和 Intent文件

数据交互

小组件和app交互是通过AppGroups的交互的

主项目和子项目都需要创建AppGroups

22.png

在主项目保存数据
Swift

// appgroup
let z = UserDefaults.init(suiteName: "你的appgroupid")
// 保存数据
z?.set("demo", forKey: "demo")
let demo = z?.value(forKey: "demo")
print("AppGroup交互 = \(String(describing: demo))")

// 全部刷新
WidgetCenter.shared.reloadAllTimelines()
 // 刷新指定的kind
// WidgetCenter.shared.reloadTimelines(ofKind: <#T##String#>)

OC 需要创建桥接文件

创建WidgetKitManager.swift
第一次的话会自动配置
WidgetKitManager.swift

import WidgetKit

@objc
@available(iOS 14.0, *)
class WidgetKitManager: NSObject {

    @objc
    static let shareManager = WidgetKitManager()
    
    /// MARK: 刷新所有小组件
    @objc
    func reloadAllTimelines() {
       #if arch(arm64) || arch(i386) || arch(x86_64)
            WidgetCenter.shared.reloadAllTimelines()
            #endif
    }

    /// MARK: 刷新单个小组件
    /*
     kind: 小组件Configuration 中的kind
     */
    @objc
    func reloadTimelines(kind: String) {
          #if arch(arm64) || arch(i386) || arch(x86_64)
        WidgetCenter.shared.reloadTimelines(ofKind: kind)
            #endif
    }
}


OC

#import "WidgetController.h"
#import "StudyOC-Swift.h"

@interface WidgetController ()

@end

@implementation WidgetController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    [self initWidget];
}

- (void)initWidget{
    UIButton *btn = [UIButton new];
    [btn setTitle:@"刷新" forState:UIControlStateNormal];
    [btn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
    btn.frame = CGRectMake(0, 100, 100, 100);
    [btn addTarget:self action:@selector(clicktBtn1) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btn];
}

- (void)clicktBtn1{
//    [WidgetCenter];
    //使用 Groups ID 初始化一个供 App Groups 使用的 NSUserDefaults 对象
    NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.zhahntest"];

    //写入数据
    [userDefaults setValue:@"123456789" forKey:@"userID"];

    //读取数据
    NSString *userIDStr = [userDefaults valueForKey:@"userID"];
    NSLog(@"zzr123 = %@",userIDStr);
    [[WidgetKitManager shareManager] reloadAllTimelines];
}

效果

一开始是空的

23.png

执行方法

24.png

刷新机制

刷新分被动刷新和主动刷新
主动刷新

WidgetCenter.shared.reloadAllTimelines()
 // 刷新指定的kind
// WidgetCenter.shared.reloadTimelines(ofKind: <#T##String#>)

被动刷新,需要就算设置1分钟,小组件也是最快5分钟刷新一次的

    // 决定 Widget 何时刷新
    func getTimeline(for configuration: AppDataIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        
        
        // 每隔2个小时刷新。
        let entry = AppDataEntry(date: Date(), configuration: configuration, str: String(describing: AppData))
            // refresh the data every two hours
        let expireDate = Calendar.current.date(byAdding: .minute, value: 5, to: Date()) ?? Date()
//        print("zzr123 = \(expireDate)")
        let timeline = Timeline(entries: [entry], policy: .after(expireDate))
        
        completion(timeline)
        
    }

点击交互

点击是通过link和widgetURL操作,格式是URL sheme

widgetUrl
widgetUrl 是针对整个小组件 点击小组件响应(如果有Link 就响应Link)

Link
LinK 给元素添加点击事件, Link 对 systemSmall样式的组件不生效(systemSmall 样式的小组件只响应widgetUrl)

struct AppDataEntryView : View {
    var entry: AppDataProvider.Entry

    var body: some View {
        VStack
        {
            // 和App数据交互
            Text(entry.str)
            Spacer()
            // 点击交互
            HStack
            {
                Link(destination: URL(string: "https://www.baidu.com?str=left")!) {
                    // 左 View
                    leftView()
                }
                Spacer()
                    .frame(width: 20)

                // 右 View
                Text("right")
                .widgetURL(URL(string: "https://www.baidu.com?str=right"))
                
            }
        }
    }
}


struct leftView : View {

    var body: some View {
        
        HStack {
            
            Text("Left")
        }
    }
}

监听是通过openUrl

SwiftUI

@main
struct SwiftUIDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onOpenURL {
                    print("交互 = \($0)")
                }
        }
    }
}

Swift

// AppDelegate
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    print("交互 = \(url)")
    return true
}

// SceneDelegate
    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
        print("交互 = \(URLContexts.first?.url)")
    }

OC

// AppDelegate
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
    NSLog(@"交互 = %@",url);
    return YES;
}

// SceneDelegate
- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts
{
    NSLog(@"交互 = %@",URLContexts.allObjects.firstObject.URL);
}

有关iOS widget 小组件开发的更多相关文章

  1. ruby - 使用 C 扩展开发 ruby​​gem 时,如何使用 Rspec 在本地进行测试? - 2

    我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当

  2. Ruby Sinatra 配置用于生产和开发 - 2

    我已经在Sinatra上创建了应用程序,它代表了一个简单的API。我想在生产和开发上进行部署。我想在部署时选择,是开发还是生产,一些方法的逻辑应该改变,这取决于部署类型。是否有任何想法,如何完成以及解决此问题的一些示例。例子:我有代码get'/api/test'doreturn"Itisdev"end但是在部署到生产环境之后我想在运行/api/test之后看到ItisPROD如何实现? 最佳答案 根据SinatraDocumentation:EnvironmentscanbesetthroughtheRACK_ENVenvironm

  3. ruby - 是否可以覆盖 gemfile 进行本地开发? - 2

    我们的git存储库中目前有一个Gemfile。但是,有一个gem我只在我的环境中本地使用(我的团队不使用它)。为了使用它,我必须将它添加到我们的Gemfile中,但每次我checkout到我们的master/dev主分支时,由于与跟踪的gemfile冲突,我必须删除它。我想要的是类似Gemfile.local的东西,它将继承从Gemfile导入的gems,但也允许在那里导入新的gems以供使用只有我的机器。此文件将在.gitignore中被忽略。这可能吗? 最佳答案 设置BUNDLE_GEMFILE环境变量:BUNDLE_GEMFI

  4. ruby - 在 Windows 机器上使用 Ruby 进行开发是否会适得其反? - 2

    这似乎非常适得其反,因为太多的gem会在window上破裂。我一直在处理很多mysql和ruby​​-mysqlgem问题(gem本身发生段错误,一个名为UnixSocket的类显然在Windows机器上不能正常工作,等等)。我只是在浪费时间吗?我应该转向不同的脚本语言吗? 最佳答案 我在Windows上使用Ruby的经验很少,但是当我开始使用Ruby时,我是在Windows上,我的总体印象是它不是Windows原生系统。因此,在主要使用Windows多年之后,开始使用Ruby促使我切换回原来的系统Unix,这次是Linux。Rub

  5. ruby-on-rails - 在 Rails 开发环境中为 .ogv 文件设置 Mime 类型 - 2

    我正在玩HTML5视频并且在ERB中有以下片段:mp4视频从在我的开发环境中运行的服务器很好地流式传输到chrome。然而firefox显示带有海报图像的视频播放器,但带有一个大X。问题似乎是mongrel不确定ogv扩展的mime类型,并且只返回text/plain,如curl所示:$curl-Ihttp://0.0.0.0:3000/pr6.ogvHTTP/1.1200OKConnection:closeDate:Mon,19Apr201012:33:50GMTLast-Modified:Sun,18Apr201012:46:07GMTContent-Type:text/plain

  6. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  7. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  8. 微信小程序开发入门与实战(Behaviors使用) - 2

    @作者:SYFStrive @博客首页:HomePage📜:微信小程序📌:个人社区(欢迎大佬们加入)👉:社区链接🔗📌:觉得文章不错可以点点关注👉:专栏连接🔗💃:感谢支持,学累了可以先看小段由小胖给大家带来的街舞👉微信小程序(🔥)目录自定义组件-behaviors    1、什么是behaviors    2、behaviors的工作方式    3、创建behavior    4、导入并使用behavior    5、behavior中所有可用的节点    6、同名字段的覆盖和组合规则总结最后自定义组件-behaviors    1、什么是behaviorsbehaviors是小程序中,用于实现

  9. ruby-on-rails - environment.rb 中设置的常量在开发模式中消失 - 2

    了解Rails缓存如何工作的人可以真正帮助我。这是嵌套在Rails::Initializer.runblock中的代码:config.after_initializedoSomeClass.const_set'SOME_CONST','SOME_VAL'end现在,如果我运行script/server并发出请求,一切都很好。然而,在我的Rails应用程序的第二个请求中,一切都因单元化常量错误而变得糟糕。在生产模式下,我可以成功发出第二个请求,这意味着常量仍然存在。我已通过将以上内容更改为以下内容来解决问题:config.after_initializedorequire'some_cl

  10. ruby - Rails 开发服务器、PDFKit 和多线程 - 2

    我有一个使用PDFKit呈现网页的pdf版本的Rails应用程序。我使用Thin作为开发服务器。问题是当我处于开发模式时。当我使用“bundleexecrailss”启动我的服务器并尝试呈现任何PDF时,整个过程会陷入僵局,因为当您呈现PDF时,会向服务器请求一些额外的资源,如图像和css,看起来只有一个线程.如何配置Rails开发服务器以运行多个工作线程?非常感谢。 最佳答案 我找到的最简单的解决方案是unicorn.geminstallunicorn创建一个unicorn.conf:worker_processes3然后使用它:

随机推荐